I'm currently looking into overloads in Typescript.

Say I have a function with one overload:

function method(): void;
function method(foo: boolean, bar: boolean): void;
function method(foo?: boolean, bar?: boolean) {
    if (foo === true || foo === false) {
        const result = bar;
    }
}

Either the function is called with no arguments, or it is called with two arguments (foo and bar). The result variable has a type of boolean | undefined according to vscode's intellisense.

How come bar can be undefined even though I have tested the foo parameter? If fooexists, shouldn't type inference predict that barexists too?

1 Answers

3
jcalz On Best Solutions

The first issue here is that the implementation signature of an overloaded function is allowed to be looser than any of the call signatures. And inside the implementation, the compiler only checks against the implementation signature. That means that inside your function, foo and bar are both independently of type boolean | undefined and there is no way to recover the fact that anyone who calls the method will specify either both or neither.

TypeScript has recently added support for rest/spread tuples in function parameters, so you can rewrite your function signature like this:

declare function method(...args: [] | [boolean, boolean]);   
method(); // okay
method(false); // errror
method(true, false); // okay

Now TypeScript knows that the args to method() are either the empty tuple or a pair of boolean values. You can keep the overloads if you want, and just make the implementation signature narrower:

function method(): void;
function method(foo: boolean, bar: boolean): void;
function method(...args: [] | [boolean, boolean]) {
  const foo = args[0];
  const bar = args[1];
  if (foo === true || foo === false) {
    const result = bar; // oops, still boolean | undefined
  }
}

Unfortunately the inference still doesn't work, and that's the second issue: TypeScript's control flow analysis is simply not as clever as we are. While we understand that the type of foo is correlated with the type of bar, the compiler does not. If narrows foo but has forgotten that bar has anything to do with foo. One way to fix this is not to break apart foo and bar into separate types, but instead use property access type guards on the single args variable. When args gets narrowed from [] | [boolean, boolean] to just [boolean, boolean], you can be sure that the second element is defined:

function method(): void;
function method(foo: boolean, bar: boolean): void;
function method(...args: [] | [boolean, boolean]) {    
    if ('0' in args) {
        const result = args[1]; // boolean
    }
}

This might all be too much code changing, and the IntelliSense isn't worth it to you. If so, and you are comfortable being smarter the compiler, you can just use a type assertion and move on with your day:

function method(): void;
function method(foo: boolean, bar: boolean): void;
function method(foo?: boolean, bar?: boolean) {
    if (foo === true || foo === false) {
        const result = bar as boolean; // I'm smarter than the compiler 
    }
}

Okay, hope that helps; good luck!