LearningMath
LearningMath

Reputation: 861

Argument type inference in TypeScript

I have the following code:

type Inferred<T> = T extends (...args: (infer UnionType)[]) => any ? UnionType : never

function f(first: 'first', second: 'second', bool: boolean) {}

type T = Inferred<typeof f> // never

I expect the inferred type to be the union type 'first' | 'second' | boolean. I am aware that I can get it this way:

type Inferred<T> = T extends (...args: infer A) => any ? A : never

function f(first: 'first', second: 'second', bool: boolean) {}

type T = Inferred<typeof f>[number] // union type

or equivalently:

type Inferred<T> = T extends (...args: infer A) => any ? A[number] : never

function f(first: 'first', second: 'second', bool: boolean) {}

type T = Inferred<typeof f> // union type

Why does the first way not work?

Upvotes: 1

Views: 554

Answers (2)

catchergeese
catchergeese

Reputation: 732

To answer the question, let's step back and see it from broader perspective.

TypeScript uses the same syntax (ie. []) for both tuples and arrays. But they are sightly different concepts. I'll be using () (instead of standard TypeScript's []) for tuples for brevity in the following paragraphs. Also, remember that T[] === Array<T> in TypeScript.

Tuples are - by convention - fixed-length and heterogeneous (holding values of possibly different types). (1, "Adam", 42) is a tuple of type (number, string, number) .

Arrays are usually homogeneous (holding value of the same type) and of arbitrary length.

In TypeScript, the difference between the two is slightly blurred because of union types support, ie. [1, "Adam", 42] is also a perfect Array<T> when T = number | string. This is not possible in languages without union types support (and would lead to finding lowest-upper bound of number | string which is usually something like any)

Using this knowledge in the provided example

type Inferred<T> = T extends (...args: (infer UnionType)[]) => any ? UnionType : never

we can see it as "get T out of function with arguments' list of type Array<T>.

We know that the type of arguments' list of: function f(first: 'first', second: 'second', bool: boolean) is ('first', 'second', boolean)

At this point, a following unification (:=:) needs to be possible:

('first', 'second', boolean) :=: Array<T>

so that values of type Array<T> can be used wherever values of type ('first', 'second', boolean) are used. And there is actually no such T, hence the inferred never.

It may seem like T = string | boolean or even T = 'first' | 'second' | boolean is a solution here but:

type Arr = Array<'first' | 'second' | boolean>
type Tuple = ['first', 'second', boolean]

const a: Arr =  ['first', 'second', true]
const t: Tuple =  ['first', 'second', true]

let a1: Arr = t;
let t1: Tuple = a; // Type error no. 2322
Target requires 3 element(s) but source may have fewer.(2322)

or on a type-level:

type TEA = Tuple extends Arr ? true : false // true
type AET = Arr extends Tuple ? true : false // false

It also explains why type Y = Inferred<(a: number, b: number, c: number) => void> yields number

To wrap up, (infer UnionType)[] in your example doesn't seem to support inferring tuple types but just arrays.

Disclaimer - I'm not familiar with TypeScript compiler internals and this is my best attempt to explain what happens here based on general knowledge and intuition.

Upvotes: 1

Aplet123
Aplet123

Reputation: 35512

It's because a function of finite arguments cannot extend a function of (potentially) infinite arguments:

// X is false
type X = 
    ((a: string, b: number) => any) extends ((...args: (string | number)[]) => any)
        ? true
        : false;

Your second two methods all use tuple types (['first', 'second', boolean]), which has a known finite length, which allows them to work.

Upvotes: 0

Related Questions