Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~26 min· topic 2 of 7

Topic 7.2

Generic Functions and Inference

In one line

A generic function can have several type parameters, and TypeScript usually works them out from the arguments. You write the type arguments by hand only when there is nothing to infer from.

Think of it like this

A vending machine that sells both snacks and drinks. You press the button and the machine gives back what you paid for. You never tell it "this is a snack purchase": it sees the button you pressed and knows. Type inference is that machine: you call the function and it works out the types from what you put in.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Type argument
The real type that fills a type parameter at a call, either written in angle brackets or inferred.
Inferred
Worked out by TypeScript from the code, with no annotation from you.
Explicit type argument
A type you write by hand in angle brackets after the function name, like pair<string, number>.
Candidate
One guess for a type parameter, collected from one argument. TypeScript combines the candidates into one answer.
Contextual type
The type that the surrounding code expects at a spot, such as the declared type of a variable receiving the result.
Context-sensitive function
An arrow function whose parameters have no annotations, so its types come from where it is used.
Generic arrow function
An arrow function with type parameters in front of its parentheses: <T>(x: T) => x.
Tuple
An array with a fixed length and a known type at each position, like [string, number].

Step by step

01Two blanks, two types

pair<A, B> has two type parameters. They're independent: A is whatever the first argument is, B whatever the second is. The return type [A, B] is a tuple that mentions both.

pair.tswhole filets
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}
const p = pair("age", 30); // [string, number]

02How the compiler finds A and B

For each argument TypeScript matches the argument's type against the parameter's type and records what the type parameter must be. first: A with "age" gives candidate string for A. second: B with 30 gives number for B.

Then it checks the whole call with those types. If two candidates for the same parameter disagree, you get an error (or a union where that's allowed, Topic 7.6).

How the compiler finds A and Bdiagram
Rendering diagram…

03When you write the arguments yourself

With pair<string, boolean>("done", true) nothing is inferred. The compiler checks the arguments against string and boolean. It's all or nothing: one type argument for two parameters is an error.

explicit.tswhole filets
pair<string, boolean>("done", true); // fine
pair<string>("only one", 1);
// error TS2558: Expected 2 type arguments, but got 1.

04Callbacks give the second parameter

map<T, U> takes an array and a function. T comes from the array's items. TypeScript then knows the callback's parameter type and **infers the callback's return type as U**.

So map(["a", "bbb"], (s) => s.length) returns number[] with no annotations. s is a string because T is string.

05Nothing to infer from

function empty<T>(): T[] never receives a T. The call empty() has no clue and gives unknown[]. Add a type argument, or let the variable's type guide it.

empty.tswhole filets
const a = empty();            // unknown[]
const b = empty<string>();    // string[]
const c: number[] = empty();  // number[] (from the variable)

06Arrow functions in TSX

Generic arrow functions in a .tsx file need a small trick, because <T> looks like JSX. A comma after T makes it a type parameter list. You'll use this in React code in Phase 14.

generic-arrow.tsxwhole filetsx
// const id = <T>(x: T) => x;      // JSX parse errors in a .tsx file
const id1 = <T,>(x: T) => x;           // fine
const id2 = <T extends unknown>(x: T) => x; // fine
function id3<T>(x: T) { return x; }    // fine anywhere

Try it yourself

  1. 1

    Predict, then read

    In the first example, predict the type of pair(true, [1, 2]) before you add it. Check with // ^?. ([boolean, number[]].)

  2. 2

    Give the variable a type

    In the second example change const e3: number[] to const e3: string[]. What does the Types panel report for e3? The same call now infers T as string, because the expected type changed.

  3. 3

    Change what the callback returns

    In lengths, change (s) => s.length to (s) => s.length > 1. Predict the result type before you look: boolean[]. U follows the return value of the function you pass.

Code & diagrams

Several type parameters, inferred or given

p1 needs no types at the call. p2 spells both out. p3 gives only one of the two and is an error (all or nothing). Delete <string> to fix it.

Console

(nothing was printed)

Expected console output

[ 'age', 30 ] [ 'done', true ]
When inference has nothing to read, and when a callback helps

e1 falls back to unknown[]. e2 and e3 get string[] and number[] from a type argument and from the variable's type. In map, T comes from the array and U from whatever the arrow function returns.

Console

(nothing was printed)

Expected console output

[] [] [] [ 1, 3, 2 ] [ { id: 1, name: 'item 1' }, { id: 2, name: 'item 2' }, { id: 3, name: 'item 3' } ]
Generic arrow functions and tuples

In a .ts file <T>(x: T) => ... is fine. swap reads a tuple, so A is string and B is number, and the result is the reverse. With as const the exact values are kept: the result is [1, "x"].

Console

(nothing was printed)

Expected console output

[ 'hi', 'hi' ] [ 1, 'x' ]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Give too few type arguments

In the first example, pair<string>("only one given", 1) is the problem. Try pair<string, boolean>("x", 1) too.

terminal
$ Types panel
── what you'll see ──
index.ts(9,17): error TS2558: Expected 2 type arguments, but got 1.

Myth vs fact

Myth

You should always write the type arguments to be safe.

Fact

Explicit type arguments often make code worse: they repeat what the compiler already knows, and they can hide a wrong argument by overriding the inferred type. Write them only when inference gives the wrong answer or has nothing to read.

Myth

Inference looks at the type you assign the result to, not at the arguments.

Fact

Arguments come first. The expected type (contextual type) is used only as an extra hint, mainly when there is nothing to read in the arguments, like empty().

Myth

<T>(x: T) => x works everywhere.

Fact

In .tsx files the compiler reads <T> as a JSX tag. Use <T,> or <T extends unknown>, or write a normal function declaration.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Inference runs in phases. First, non-context-sensitive arguments produce candidates. Then context-sensitive arrow functions are typed using the types fixed so far, and may add more candidates (for U). This is why map(arr, (x) => ...) can use T from arr inside the callback, and why changing the order of arguments in your own signature sometimes changes whether a callback sees its parameter type.

  • ▸

    If a type parameter has several candidates, TypeScript looks for one candidate that all the others are assignable to (the best common type). If there is none, it fixes the type from the first candidate and reports an error at the argument that doesn't fit. Topic 7.6 shows this with same("a", 1).

  • ▸

    The <T,> trick is only syntax: a trailing comma in a type parameter list is allowed and tells the parser it isn't JSX. TypeScript 6.0 and 7.0 both accept it. <T extends unknown> does the same job and reads more clearly to people who haven't seen the comma before.

Remember this

  1. 1

    The problem this solves: Real functions often relate two or more types, such as a pair, and writing every type by hand at each call is tedious and error-prone. Recall from Topic 7.1: a type parameter is a blank for a type. A function can have more than one type parameter, separated by commas: function pair<A, B>(first: A, second: B): [A, B]. Each parameter is its own blank. Here A is the type of the first argument and B the type of the second, so pair("age", 30) returns [string, number]. Use one type parameter per thing that can vary on its own. Two arguments that must match use the same one (<T>(a: T, b: T), Topic 7.6).

  2. 2

    Inferred type arguments are the default. TypeScript looks at every place a type parameter appears in the parameters, collects a candidate from each argument, and chooses the type that fits. It works with objects, arrays, and with the return type of a callback: in map<T, U>(items: T[], fn: (item: T) => U): U[], T comes from the array and U comes from what the arrow function returns. You write no types at the call and still get number[] or an array of objects.

  3. 3

    Explicit type arguments go in angle brackets before the parentheses: pair<string, boolean>("done", true). Write them when you want a type that differs from the one inferred (a wider or more specific one), or when nothing can be inferred. The rule is all or nothing: you can't give just the first of two (pair<string>(...) is an error, TS2558, "Expected 2 type arguments, but got 1"), unless the remaining parameters have defaults (Topic 7.4).

  4. 4

    When there is nothing to infer from, TypeScript falls back to unknown. function empty<T>(): T[] has no argument that mentions T, so empty() is unknown[]. Two ways out: pass the type by hand (empty<string>()), or give the call a contextual type (a type the surrounding code already expects). With const e: number[] = empty() the compiler sees the variable needs number[] and infers T as number. This is inference from the return type, more in Topic 7.6.

  5. 5

    Generic arrow functions put the type parameters before the parentheses: const twice = <T>(x: T): [T, T] => [x, x]. In a .ts file this is fine. In a .tsx file (React, Phase 14) there is a clash: <T> looks like the start of a JSX tag, so the compiler reads <T>(x: T) => x as an unclosed <T> element and reports errors such as TS17008 ("JSX element 'T' has no corresponding closing tag"), TS1382 and TS1005. The usual fixes are a trailing comma, <T,>(x: T) => x, or a constraint, <T extends unknown>(x: T) => x. Declared functions (function id<T>(x: T)) have no problem.

  6. 6

    Inference works left to right on arguments that carry type information, and it postpones arrow functions whose own parameters have no annotation (they are called context-sensitive): in apply((x) => ..., 42) TypeScript first reads 42, decides T is number, and then types x as number. This is why putting the plain value argument anywhere in the call works, and why the order of parameters in your own generic functions rarely matters for callers.

  7. 7

    Inference is a hint you can check: hover or use // ^? on the call. If the inferred type surprises you, that is useful information. It means either the argument isn't what you thought, or you need to guide the compiler with an explicit argument, a constraint (Topic 7.3) or a tool from Topic 7.6.

Explain it without notes

01

How does TypeScript infer type arguments? What happens when it can't?

02

When should you write explicit type arguments?

03

Why does function empty<T>(): T[] give unknown[] when called as empty(), and how do you fix it?

04

Why does a generic arrow function need <T,> in a TSX file?

Practice

01

Write a generic zip that takes two arrays and returns an array of pairs [A, B][] (stop at the shorter one). Call it with numbers and text.

02

Write makeArray<T>(value: T, times: number): T[] and call it so that the result is string[], then so it is (string | number)[].

03

Write a generic arrow function firstOr that takes an array and a fallback and returns the first item or the fallback. Does it need <T,> in a .ts file?

Trade-offs

  • ↔

    Inference keeps call sites clean, but it can make errors appear far from the cause: a wrong argument may change T and the error shows up later. When an error looks strange, use // ^? on the call to see what was inferred.

  • ↔

    Explicit type arguments are precise but brittle: they must be updated when the function's type parameters change. Prefer inference and add an explicit type only where it removes ambiguity.

Done when you can

  • I can write a function with two type parameters and say which argument fills each.

  • I can call a generic function with inferred and with explicit type arguments.

  • I can explain why empty() gives unknown[] and fix it two ways.

  • I can say why pair<string>(...) is an error.

  • I can write a generic arrow function that also works in a .tsx file.