27

Is this possible to have types restricted without if by function calls that never return for e.g undefined like assert in Typescript?

Example code:

interface Foo { bar(): void }
function getFoo(): Foo | undefined { }

function test() {
    const foo = someService.getFoo();
    assert(foo);
    if (!foo) { // now mandatory because without this foo may be still undefined even if assert protects us from this
        return;
    }
    foo.bar(); // , here foo may be undefined
}

I would like to be able to write assert in such way that i can skip following if (!foo) clause and have foo type restricted to plain Foo.

Is this possible in Typescript?

I've tried adding overloads with never for types that throw:

function assertGuard(v: undefined | null | '' | 0 | false): never;
function assertGuard(v: any): void; // i'm not sure which one is  captured by TS typesystem here

function assertGuard<T>(v: T | undefined) {
    if (v === undefined || v === null || v === '' || v === 0 || v === false) {
         throw new AssertionError({message: 'foo'})
    }
}

This one compiles, but call to assertGuard(foo) doesn't recognize that for undefined it will return never so doesn't restrict foo to Foo.

I've found possible workarounds but i consider classical assert a cleaner approach:

function assertResultDefined<T>(v: T|undefined): T | never {
    if (v === undefined) {
        throw new Error('foo');
    }
    return v;
}
function die(): never { throw new Error('value expected)}

const foo = assertResultDefined(getFoo()) // foo is Foo, undefined is erased
const foo = getFoo() || die();
    // undefined is erased from foo
    / CONS: doesn't play well with types that interpolate to `false` like 0, ''
1
  • 2
    Note, this question is outdated because TypeScript 3.7 introduced very special syntax for problem raised in question, see this answer: stackoverflow.com/a/59017341/269448 Commented Nov 25, 2019 at 13:16

5 Answers 5

45

Typescript 3.7 adds assertions in control flow analysis.

An asserts return type predicate indicates that the function returns only when the assertion holds and otherwise throws an exception

Hacks on consumer side are not needed anymore.

interface Foo { bar(): void }
declare function getFoo(): Foo | undefined;

function assert(value: unknown): asserts value {
    if (value === undefined) {
        throw new Error('value must be defined');
    }
}

function test() {
    const foo = getFoo();
    // foo is Foo | undefined here
    assert(foo);
    // foo narrowed to Foo
    foo.bar();
}

Playground


Additionally one can assert that provided parameter is of required type:

declare function assertIsArrayOfStrings(obj: unknown): asserts obj is string[];

function foo(x: unknown) {
    assertIsArrayOfStrings(x);
    return x[0].length;  // x has type string[] here
}

Playground

Sign up to request clarification or add additional context in comments.

1 Comment

Yep, we all probably know this great feature of 3.7, i'll update comment to mark that question is outdated.
9

There is an issue in the typescript backlog for this https://github.com/Microsoft/TypeScript/issues/8655. So for now you can't do this.

What you can do, is to use the assertion operator "!". Adding ! after value will assert that the value is neither undefined nor null. Use this is case where you're absolutely sure it cannot lead to a null or undefined reference.

function test() {
     const foo: (FooType|null) = getFoo();
     foo!.bar(); // "!" - asserts that foo is not null nor undefined
}

Source: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-type-assertions

1 Comment

Thanks. I was almost sure that this was impossible and wanted to write proposal as i wasn't able to find existing one. Thanks for saving me effort to write proposal only to have it marked as dup.
2

Since foo is Foo | undefined, its type should be changed to Foo somehow.

In the code above, this reasonably can be done with:

let foo = getFoo(); // Foo | undefined
foo = assertResultDefined(foo); // Foo
foo.bar();

Another option is to use non-null assertion (as another answer suggests):

let foo = getFoo();
foo = assertResultDefined(foo);
foo = foo!;
foo.bar();

2 Comments

"type should be changed to Foo somehow" Thanks for reply. Maybe i wasn't explicit enough - i need it for testing code where Foo | undefined is part of existing design (maybe good, maybe not, but it's more or less fixed). I already strive to remove all undefineds from code but it's not always possible. Nevertheless, hints are cool. +1
Yes, that's exactly what I mean. Foo | undefined makes sense, foo type just needs to be converted to Foo at some point to access object properties freely.
1

this should work for you:

const foo = (a: number | null) => {
  a = shouldBe(_.isNumber, a)
  a  // TADA! a: number
}

const shouldBe = <T>(fn: (t1) => t1 is T, t) => (fn(t) ? t : throwError(fn, t))

const throwError = (fn:Function, t) => {
  throw new Error(`not valid, ${fn.name} failed on ${t}`)
}

where _.isNumber has a type guard x is number This can be used with any function with a type guard.

the key is you must reassign the variable, so effectively assert is an identity function that throws an error on failed type assertion

1 Comment

I'm using this hack from time to time in various other contexts, but still it's a hack :) Thanks anyway.
0

One-liner with type safety:

function assert<T>(value: T | undefined): T {
  if (value === undefined) {
    throw new Error('value is undefined');
  }

  return value;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.