Search code examples
javascripttypescriptnarrowingtype-narrowing

Can I narrow this down in TypeScript?


I have a utility function to check whether a variable is not null or undefined, and I want TypeScript to narrow down the input variable if it passes the check, for example:

public init(input?: string): void {
    function isSpecified(input: any): boolean {
        return (typeof input !== "undefined") && (input !== null);
    }

    if (isSpecified(input)) {
        let copiedString: string = input; // <-- Error; input is still 'string | undefined'
    }
}

As you can see TS is not removing the possibility of the string being undefined even though the function makes that logically impossible. Is there a way I can get this function call to narrow down input inside the if block?


Solution

  • You can use a generic type guard function:

    public init(input?: string): void {
        function isSpecified<T>(input: null | undefined | T): input is T {
            return (typeof input !== "undefined") && (input !== null);
        }
    
        if (isSpecified(input)) {
            let copiedString: string = input; // OK
        }
    }
    

    Note that if TypeScript narrows down a variable's union type through narrowing during an assignment, isSpecified will still not act as a type guard. For example, in the following code, TS will give an error:

    someMethod = () => {
        const foo: undefined | null | number = undefined;
        if (isSpecified(foo)) {
            this.nonNullValue = foo; // Error
        }
    };
    

    In the above, foo being assigned as undefined narrows the variable type and the T in isSpecified is undefined, so TS still doesn't treat foo as definitely being specified. This can be worked around by using a type assertion (val as ...) to prevent narrowing:

    someMethod = () => {
        //const foo = 123 as
        // ... or:
        const foo = undefined as
            undefined | null | number;
        if (isSpecified(foo)) {
            this.nonNullValue = foo;
        }
    };