Fuzzyma
Fuzzyma

Reputation: 8484

Infer return type of function based on type guard

I have the following definition:

function foo(arg: number[] | number) {
    if (Array.isArray(arg)) {
        return [1]
    }

    return 0
}

I would expect typescript to figure out automatically what the return type is. Because of the type guard isArray() it knows if arg is an Array and can show the return type as number[]. However, using foo(...) shows its return value as number[] | 0 even when passing an array.

  foo([]).push() // error because push doesnt exist on type 0

Is this a design limitation, a bug, just not implemented yet, or some other issue?

Upvotes: 3

Views: 156

Answers (3)

bela53
bela53

Reputation: 3485

An alternative are Generics:

function foo<T extends number[] | number>(arg:T):T {
    if (Array.isArray(arg)) { return [1] as T }
    return 0 as T
}

foo([] as number[]).push(10) // works
foo(42).push(10) // error as expected

The consumer of foo gets perfect types. Inside we use a type assertion, as the returned values are not sound. The shape of T is specified by the caller, so we cannot know, what exactly it is.

Imagine, the club of number 42 fetishists owns foo - all other numbers are mehh... :)

Then something like
function foo<T extends number[] | number>(arg:T):T {
    if (Array.isArray(arg)) { return [1]}
    return 1
}
foo(42); // T instantiated as 42

clearly wouldn't be tolerated.

While the example is contrived - the club sounds neat though -, you probably get the point, why first example needs to be type asserted.

A more realistic, sound sample:
function ensureDefined<T extends number[] | number>(initializer: () => T, arg?: T): T {
    if (arg === undefined || Array.isArray(arg) && arg.length === 0) {
        return initializer()
    } else return arg
}

ensureDefined(() => [42], [] as number[]).push(10) // works
ensureDefined(()=> 42, undefined) // error as expected

Here is a Playground.

Upvotes: 0

Philip Oliver
Philip Oliver

Reputation: 156

I think what you're looking for is overloads.

Using overloading as followed would solve your problem:


function foo(arg1: number[]): number[];
function foo(arg1: number): 0;
function foo(arg1: any){
    if(Array.isArray(arg1)){
        return [0]
    }

    return 0
}

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074949

I can't point to something definitively saying it's a design limitation, but I've seen experts like jcalz and Titian Cernicova-Dragomir citing various places where type inference is limited sometimes not because it couldn't do what we want, but because it would be too costly to do it (in terms of runtime cost or code complexity in the compiler). I suspect this fits into that category.

You probably know this, but for your specific example, you can use overloads to get the result you want:

function foo(arg: number[]): number[];
function foo(arg: number): number;
function foo(arg: number[] | number) {
    if (Array.isArray(arg)) {
        return arg.map(v => v * 2);
    }
    
    return arg * 2;
}
foo([]).push(10);

Playground link

Upvotes: 5

Related Questions