Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript assert-like type guard

Tags:

typescript

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, ''
like image 416
Zbigniew Zagórski Avatar asked Mar 19 '18 12:03

Zbigniew Zagórski


People also ask

Is type guard TypeScript?

A type guard is a TypeScript technique used to get information about the type of a variable, usually within a conditional block. Type guards are regular functions that return a boolean, taking a type and telling TypeScript if it can be narrowed down to something more specific.

What is ?: In TypeScript?

What does ?: mean in TypeScript? Using a question mark followed by a colon ( ?: ) means a property is optional. That said, a property can either have a value based on the type defined or its value can be undefined .

What is narrowing in TypeScript?

TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing.

What is type alias in TypeScript?

In Typescript, Type aliases give a type a new name. They are similar to interfaces in that they can be used to name primitives and any other kinds that you'd have to define by hand otherwise. Aliasing doesn't truly create a new type; instead, it gives that type a new name.


4 Answers

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

like image 110
Aleksey L. Avatar answered Sep 29 '22 09:09

Aleksey L.


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

like image 32
gawicks Avatar answered Sep 29 '22 08:09

gawicks


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();
like image 20
Estus Flask Avatar answered Sep 29 '22 08:09

Estus Flask


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

like image 33
bkucera Avatar answered Sep 29 '22 09:09

bkucera