microsoft / microsoft/TypeScript

Function parameters and return type inference with infer keyword

Open
#38,182 2 comments 10 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

In Discussion Suggestion
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

I saw a couple of similar issues, but it all provided very poor specification of the problem, so I decided to open my own issue.

Description

The further step in bringing nice fp declarations into TypeScript is adding function parameters and return type inference through infer keyword.

Lets look at some examples:

type F1 = <T extends number>(a: T, b: T) => T

// Now: [number, number]
// Propose: [1, 1]
type R1 = F1 extends ((a: 1, b: infer P) => infer R) ? [P, R] : never

Here we passed parameter a as 1 and expect:

  1. Type parameter T to be inferred as 1
  2. Type P to be inferred as T -> 1
  3. Type R to be inferred as T -> 1
  4. Result to be inferred as [P, R] -> [1, 1]

type F2 = <T1 extends AnyTuple, T2>(a: T1, b: T2, c: Reverse<T1>) => [T1, T2]  

// Now: [never, unknown, [AnyTuple, unknown]]
// Propose: [[3, 2, 1], unknown, [[1, 2, 3], unknown]]
type R2 = F2 extends (a: [1, 2, 3], b: infer P2, c: infer P1) => infer R ? [P1, P2, R] : never

Here we passed parameter a as [1, 2, 3] and expect:

  1. Type parameter T1 to be inferred as [1, 2, 3]
  2. Type parameter T2 to be replaced with unknown type because we provided no info to be able to infer it
  3. Type P2 to be inferred as T2 -> unknown
  4. Type P1 to be inferred as Reverse<T1> -> Reverse<[1, 2, 3]> -> [3, 2, 1]
  5. Type R to be inferred as [T1, T2] -> [[1, 2, 3], unknown]
  6. Result to be inferred as [P1, P2, R] -> `[[3, 2, 1], unknown, [[1, 2, 3], unknown]]

type F3 = <T extends AnyTuple>(a: T, b: T['length']) => typeof b

// Now: never
// Propose: 3
type R3 = F3 extends ((a: [1, 2, 3], b: infer LEN) => any) ? LEN : never

Here we expect:

  1. Type parameter T to be inferred as [1, 2, 3]
  2. Type LEN to be inferred as T['length'] -> [1, 2, 3]['length'] -> 3
  3. Result to be inferred as LEN -> 3

Case of wrapping type checkers:

enum string_literal { string_literal = '' }

type StringLiteral<T> = T extends string ? string extends T ? string_literal : T : string_literal

type F4 = <T>(a: StringLiteral<T>, b: T) => T extends '' ? [] : T

// Now: all cases never

// Proposed: ['foobar', 'foobar']
type R4_1 = F4 extends ((a: 'foobar', b: infer P) => infer R) ? [P, R] : never
// Proposed: ['', []]
type R4_2 = F4 extends ((a: '', b: infer P) => infer R) ? [P, R] : never
// Proposed: [string_literal, string]
type R4_3 = F4 extends ((a: string, b: infer P) => infer R) ? [P, R] : never

Use cases

In other similar issues I saw only use cases of function parameters inference, so I'll extend it with return type inference cases.

The first example is a call function:

declare function call<P extends AnyTuple, F extends ((...args: P) => unknown)>(
  params: P, func: F
): F extends (...args: P) => infer R ? R : never

// Got: [unknown, unknown]
// Expected: [1, 2]
const a = call([1, 2], <A, B>(a: A, b: B): [A, B] => [a, b])

Also it may be used to infer return types on each step of computing pipe and flow function versions with unlimited parameters count. I'm sure, there're much more cases where this feature may be applied, but I can't remember any more right now.

Util types used

type Empty = readonly []


type AnyTuple = ReadonlyArray<unknown> & { readonly 0: unknown } | Empty


type Tail<T extends AnyTuple> = ((...args: T) => any) extends (head: any, ...tail: infer R) => any

  ? R extends AnyTuple ? Readonly<R> : never : Empty


type Prepend<T extends AnyTuple, E> = ((e: E, ...args: T) => any) extends

  (...args: infer R) => any ? R extends AnyTuple ? Readonly<R> : never : never


type _ReverseWith<T extends AnyTuple, Result extends AnyTuple> = {
  0: _ReverseWith<Tail<T>, Prepend<Result, T[0]>>
  1: Readonly<Result>
}[T extends Empty ? 1 : 0]


type Reverse<T extends AnyTuple> = _ReverseWith<T, Empty>

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the conditional-type examples and the documented current versus proposed results in the issue. No implementation files, tests, or entry points are named, so locating the relevant type-inference subsystem and defining compatible tests is part of the work. Done means the proposed parameter and return type inference cases produce the stated results.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.