Search code examples
typescriptnullableassertions

Can I write a type guard that throws exceptions instead of returning booleans?


I have a class that uses the same type guard in multiple functions; something like this:

function validData(d: Data | null): d is Data {
    return d !== null;
}

class C {
    data: Data | null;

    public doA() {
        if (!validData(this.data))
            throw new Error("Invalid data");
        /* … */
    }

    public doB() {
        if (!validData(this.data))
            throw new Error("Invalid data");
        /* … */
    }
}

Can I refactor this code to move the error into the type guard? Something like this:

function assertData(d: Data | null): ??? {
    if (d === null)
        throw new Error("Invalid data");
}

…which I could use like this:

class C {
    data: Data | null;

    public doA() {
        assertData(this.data);
        /* … */
    }

    public doB() {
        assertData(this.data);
        /* … */
    }
}

Currently I'm using the following workaround:

function must(d: Data | null): Data {
    if (d === null)
        throw new Error("Invalid data");
    return d;
}

… but this forces me to wrap every access to this.data in must().


Solution

  • Edit Since the original answer, typescript has added the ability for custom type assertions in this PR

    type Data = { foo: string };
    
    function assertData(d: Data | null): asserts d is Data {
        if (d == null)
            throw new Error("Invalid data");
    }
    // Use
    declare var bar: Data | null;
    bar.foo // error as expected
    assertData(bar)
    bar.foo // inferred to be Data
    
    

    Playground Link

    Original answer

    Unfortunately the current syntax for type guards requires an if statement for them to work. So this works

    type Data = { foo: string };
    function assertData(d: Data | null): d is Data {
        if (d == null)
            throw new Error("Invalid data");
        return true;
    }
    // Use
    let bar: Data | null = null;
    if (assertData(bar)) {
        bar.foo // inferred to be Data
    }
    

    But there is no way to get this to work:

    let bar: Data | null = null;
    assertData(bar);
    bar.foo // bar will still be Data | null