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.
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).
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.
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.
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.
// 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 anywhereTry it yourself
- 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
Give the variable a type
In the second example change
const e3: number[]toconst e3: string[]. What does the Types panel report fore3? The same call now infersTasstring, because the expected type changed. - 3
Change what the callback returns
In
lengths, change(s) => s.lengthto(s) => s.length > 1. Predict the result type before you look:boolean[].Ufollows the return value of the function you pass.
Code & diagrams
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 ]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' } ]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.
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 whymap(arr, (x) => ...)can useTfromarrinside 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
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. HereAis the type of the first argument andBthe type of the second, sopair("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
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[],Tcomes from the array andUcomes from what the arrow function returns. You write no types at the call and still getnumber[]or an array of objects. - 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
When there is nothing to infer from, TypeScript falls back to
unknown.function empty<T>(): T[]has no argument that mentionsT, soempty()isunknown[]. Two ways out: pass the type by hand (empty<string>()), or give the call a contextual type (a type the surrounding code already expects). Withconst e: number[] = empty()the compiler sees the variable needsnumber[]and infersTasnumber. This is inference from the return type, more in Topic 7.6. - 5
Generic arrow functions put the type parameters before the parentheses:
const twice = <T>(x: T): [T, T] => [x, x]. In a.tsfile this is fine. In a.tsxfile (React, Phase 14) there is a clash:<T>looks like the start of a JSX tag, so the compiler reads<T>(x: T) => xas an unclosed<T>element and reports errors such asTS17008("JSX element 'T' has no corresponding closing tag"),TS1382andTS1005. 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
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 reads42, decidesTisnumber, and then typesxasnumber. 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
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
How does TypeScript infer type arguments? What happens when it can't?
When should you write explicit type arguments?
Why does function empty<T>(): T[] give unknown[] when called as empty(), and how do you fix it?
Why does a generic arrow function need <T,> in a TSX file?
Practice
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.
Write makeArray<T>(value: T, times: number): T[] and call it so that the result is string[], then so it is (string | number)[].
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
Tand 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()givesunknown[]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
.tsxfile.