Topic 7.1
Why Generics
In one line
A generic function has a blank for the type. The caller fills the blank, and TypeScript remembers it, so what goes in is exactly what comes out, with no copying and no any.
Think of it like this
A school lunchbox with a clear window and a removable tray. The box itself does not care what is inside. Put a sandwich in and you get a sandwich out. Put fruit in and you get fruit out. The label on the window changes with the food, but the box is built once. A generic is that box: one definition, and the type of what it holds is filled in each time you use it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generic
- Code (a function, interface, type alias or class) written with a blank for the type, so it works for many types and keeps track of which one.
- Type parameter
- The blank itself, declared in angle brackets after the name, like
<T>. It stands for a type that is decided later. - Type variable
- Another name for a type parameter. It is a variable whose value is a type.
- Type argument
- The real type that fills the blank, as in
identity<string>. Also what TypeScript works out for you. - Inference
- TypeScript working out a type by itself, here by reading the arguments of a call.
- Identity function
- A function that returns exactly what it was given. The smallest example of a generic.
- any
- A type that switches checking off. A value of type
anycan be used in any way, and mistakes are only found when the code runs. - Erased
- Removed during compilation. Type parameters, like all types, do not exist in the JavaScript that runs.
Step by step
01The same code, three times
Without generics, one function per type. The bodies are identical. Only the types change.
function identityNumber(value: number): number {
return value;
}
function identityString(value: string): string {
return value;
}
function identityBoolean(value: boolean): boolean {
return value;
}
// ...and again for every new type02The shortcut that loses everything: any
One function, any input. But the output type is any too, so TypeScript can no longer tell you anything about it.
The checker can't warn you if you call a text method on a number. The mistake survives until a user hits it.
function identityAny(value: any): any {
return value;
}
const n = identityAny(42);
n.toUpperCase(); // no error. Crashes when it runs.03Declare a type variable
Put <T> after the function name. Now T exists as a type inside the signature and the body. Use it in the parameter and in the return type to say "these are the same type".
function identity<T>(value: T): T {
return value;
}04Each call fills the blank
TypeScript looks at the argument and sets T. Each call gets its own T, so the same function returns "hello" for text and 42 for a number. Nothing is copied. TypeScript substitutes the type.
05Why the same T matters
The power is the repeated T. The signature (value: T): T is a promise that the output is the same kind as the input. With any there is no link between them.
You can give the blank by hand: identity<string>("hello"). If the argument does not match, you get an ordinary type error, because T is now fixed to string.
06What you can't do inside
Because T could be anything, the body may only do what is valid for everything. value.length fails. Returning something else, like String(value), fails too: it says T could be a number and the string is not a number.
This restriction is a feature: it proves the function can't cheat. You will loosen it on purpose with constraints in Topic 7.3.
Try it yourself
- 1
Widen with let
In the first example change
const btolet b. Predict the new type, then look. Because aletcan be reassigned, TypeScript widens"hello"tostring. - 2
Make the two lists differ
In the second example change
first([10, 20, 30])tofirst(["a", "b"]). What doessafebecome? IstoUpperCasestill an error? (It is still "possibly undefined", but the second message goes away.) - 3
Name the blank anything
Rename
TtoItemeverywhere inidentity. Nothing changes:Tis only a name. Then try returningvalue.lengthand read the error.
Code & diagrams
The panel shows a is any, but b is the exact text type "hello" and c is 42. Change const b to let b and ask again: it widens to string.
Console
(nothing was printed)Expected console output
hello hello 42Both calls are mistakes (numbers have no toUpperCase). any lets the first one through, so it crashes at runtime. The generic reports the second one before running: the result is number | undefined, so it is possibly undefined and a number has no toUpperCase. The code still runs and shows the crash the first line caused.
Console
(nothing was printed)Expected console output
Uncaught TypeError: risky.toUpperCase is not a functionOnce you write the type argument, T is fixed. identity<number> wants a number, so the text on line 6 is an error. The code still runs, because types are erased.
Console
(nothing was printed)Expected console output
hello trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Cheat on the return type
In identity<T>, change the body to return String(value);.
Break #2
Forget to declare T
Delete the <T> after the function name, keeping (value: T): T.
Myth vs fact
Myth
T is a special keyword.
Fact
It's an ordinary name chosen by habit. function identity<Thing>(value: Thing): Thing is identical. Short names are conventional for short generics; use descriptive names (TItem, Key) when there are several or the meaning is not obvious.
Myth
Generics are the same as any.
Fact
any turns checking off and forgets the type. A generic keeps the exact type and checks it. The only thing they share is that both accept many kinds of input.
Myth
Generic code runs slower or leaves something behind at runtime.
Fact
Types are erased. The JavaScript output for a generic function is the same plain function. There is no type information left to cost anything.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Inference picks the narrowest type that fits each argument, and keeps a literal such as
"hello"when the type parameter is used at the top level of the return type. That is whyidentity("hello")gives"hello"butwrap("hi")(which returns{ value: T }) widens to{ value: string }. Aletdeclaration widens the literal afterwards. - ▸
Generics are parametric: a function that knows nothing about
Tcan't behave differently by type, which is why the body is so restricted. This is the same idea as "free theorems" in functional languages: the signature<T>(x: T) => Thas essentially one possible implementation. - ▸
unknownis the safe cousin ofany(Topic 1.6): use it when you really don't know the type. Reach for a generic when the output depends on the input, andunknownwhen it does not.
Remember this
- 1
The problem this solves: A function that works for any kind of value would need one copy per type, or the type
any, which turns the checks off. Generics keep one function and still track the type. Start with the smallest function there is, the identity function: it returns whatever it was given. For numbers you would writefunction identity(value: number): number. For text you would write a second copy withstring. Then one for booleans, users, dates. Copying is slow and every copy can drift. The problem generics solve is repeating the same logic just because the type changed. - 2
The lazy fix is
any:function identity(value: any): any. It is one function and it accepts everything, but it loses information.anymeans "switch the checker off for this value". The result ofidentity("hello")isany, notstring, so.toUpperCase()and.toFixed()are both allowed, and the wrong one crashes at runtime. You wrote a type and got no checking. - 3
The real fix is a type parameter:
function identity<T>(value: T): T. The<T>after the name declares a type variable calledT. It is a variable, but its value is a type. It is a blank that the caller fills in. The rest of the signature says: whatever typeTturns out to be, the parameter has that type, and the return value has that same type. The nameTis only a habit (short for "type"). Any name works, and longer names such asItemorTKeyare fine. - 4
Who fills the blank? Usually nobody types anything: TypeScript reads the argument and works it out. For
identity("hello")it sees the text"hello"and setsTto"hello". Foridentity(42),Tis42. This is type argument inference. You can also give it by hand,identity<string>("hello"): the part in angle brackets is the type argument. Topic 7.2 covers when to leave it out and when to write it. - 5
Notice the type is
"hello"(the exact text), notstring. That is not a mistake: aconstresult can never change, so TypeScript keeps the precise value (Topic 1.3 explained this). If the result is stored in alet, it widens tostring. This is the first thing you learn from// ^?: **generics keep exact information thatanythrows away.** - 6
A generic function never looks inside
T. Inside the function bodyTcould be a number, an object,null, anything, so you can only do what is safe for every type: pass it around, put it in an array, return it.value.lengthwould be an error because not everyThas alength. Topic 7.3 shows how to narrow whatTmay be. - 7
Generics are erased like all types: after compilation
identity<T>is plainfunction identity(value) { return value; }. There is nothing generic at runtime, and you can't writenew T()ortypeof T. Generics are a compile-time tool for relating types to each other: "the output type depends on the input type".
Explain it without notes
What problem do generics solve, and why isn't any the answer?
What is a type parameter, and what is the difference between it and a type argument?
Why does identity("hello") return type "hello" and not string?
Do generics exist at runtime?
Practice
Write a generic function last that returns the last item of an array (or undefined if it is empty). Call it with numbers and with text and check both result types.
Write a generic wrap that takes any value and returns an object { value }. What is the type of wrap("hi")?
This function loses information. Rewrite it with a type parameter and show the type of the result is no longer any.
``ts
function second(items: any[]): any {
return items[1];
}
Trade-offs
- ↔
A generic signature is more abstract than a concrete one. Beginners read
<T>(items: T[]): T | undefinedmore slowly than(items: number[]): number | undefined. Use generics when the same logic really is used with several types, not "just in case". - ↔
A type parameter that appears only once in the signature adds nothing:
function log<T>(x: T): voidis no better thanfunction log(x: unknown): void. A type parameter earns its place by linking two or more places (input to output, or input to input).
Done when you can
I can explain, with the lunchbox idea, what a generic is.
I can say why
anyloses information and a type parameter does not.I can write
function identity<T>(value: T): Tand read it aloud.I can say what
Tis: a name for a type that the caller fills in.I can use
// ^?to see whatTwas inferred as.I can say why generics do not exist at runtime.