Typescript: Inconsistent inference from a rest parameter

Created on 9 Apr 2019  ·  3Comments  ·  Source: microsoft/TypeScript


TypeScript Version: 3.5.0-dev.20190407 and 3.4 in TS playground


Search Terms: infer rest tuple

Code

type Action<T extends string = string> = { type: T };

declare function f<A extends Action>(...args: A[]): A[];
declare function g<A extends Action>(args: A[]): A[];

const A = "A";
const B = "B";
const C = "C";
declare const a: Action<typeof A>;
declare const b: Action<typeof B>;
declare const c: Action<typeof C>;

// Here b effects an error:
const r1 = f(a, b, c);

// Here r2 is inferred correctly:
const r2 = f(
    { type: A },
    { type: B },
    { type: C }
);

// Here r3 is inferred correctly:
const r3 = f(...[a, b, c]);

// Here r4 is inferred correctly:
const r4 = g([a, b, c]);

// Here r5 is inferred correctly:
const r5 = g([
    { type: A },
    { type: B },
    { type: C }
]);

Expected behavior:

In all of the calls to f and g in the example above, I would have expected no errors and the result to be inferred correctly.

Actual behavior:

Arguments passed to the function that accepts a rest parameter - f - work only if said arguments are literals or are spread from an array/tuple. If separate, declared variables are passed, an error is effected, stating that the arguments are expected to be of the same type.

Both variables and literals behave as expected with g - a function that accepts an array parameter, rather than a rest parameter.

Playground Link:

Here is the playground link

Related Issues: None found

Working as Intended

Most helpful comment

Thanks. BTW, the workaround appears to be this.

type Action<T extends string = string> = { type: T };

declare function f<A extends Action, R extends A[]>(...args: R): R;

const A = "A";
const B = "B";
const C = "C";
declare const a: Action<typeof A>;
declare const b: Action<typeof B>;
declare const c: Action<typeof C>;

const r1 = f(a, b, c);

All 3 comments

This is an intentional difference.

Consider this code:

type Action<T extends string = string> = { type: T };
declare const a: Action<"A">, b: Action<"B">, c: Action<"C">;

declare function f<T extends Action>(...args: T[]): T[];
declare function g<T extends Action>(args: T[]): T[];
declare function h<T>(a: T, b: T, c: T): T[];

g([a, b, c]);
f(a, b, c); // ?
h(a, b, c);

g has to be OK because heterogeneous array literals are fine.

h should clearly be an error - otherwise generics don't mean anything (consider a function like insert<T>(arr: T[], el: T): void and calling insert([1, 2, 3], "4") - it'd be a disaster to infer T = string|number).

The question is whether it's more correct/consistent for f to behave like g or like h -- certainly you can have your own interpretation, but we chose that a rest arg array should behave more like an infinite set of formally-declared parameters than an array literal passed inline.

Thanks. BTW, the workaround appears to be this.

type Action<T extends string = string> = { type: T };

declare function f<A extends Action, R extends A[]>(...args: R): R;

const A = "A";
const B = "B";
const C = "C";
declare const a: Action<typeof A>;
declare const b: Action<typeof B>;
declare const c: Action<typeof C>;

const r1 = f(a, b, c);

The above workaround can be simplified; the A extends Action generic in the other workaround is unused, as A is always inferred to be of type Action<string>. Only the R generic is usefully inferred, so the function signature can be reduced to:

// ...
declare function f<R extends Action[]>(...args: R): R;
// ...

Playground Link

Was this page helpful?
0 / 5 - 0 ratings