Generics: one function, many types
Do not commit to a type yet. Write a placeholder, <T>, and let the call site decide what it stands for. Every T in one signature is the same T, so the type of the result stays tied to the type of the input.
The problem: three bad versions of one small function
The task is as small as it gets: return the first element of an array. Without generics there is no good way to write it once.
One action, three different arrays
A small shop's ordering system needs the first order in a list, the first item on a menu, and the first order in a history. It is the same action three times, over three different array types. Without generics you have three ways to write it, and all three are unsatisfying.
Look at the right-hand version. One function handles every array type, and the input and the output stay linked: give it a string[] and you get back something built from string; give it an Order[] and you get back something built from Order. That link is what the middle version threw away. The next section slows a single call down to show how it is made.
Leave a hole: what <T> actually does
T is a placeholder for a type. It is filled in at the call site, not where the function is written. Because one signature reuses the same placeholder, the compiler can keep the input and the output in step.
<T> declares the placeholder; the two later Ts are uses of that same placeholder. Nothing is filled in yet. Press Next to bring in a call.The angle brackets after the function name declare one placeholder and give it the name T. It is called a type parameter: a parameter like any other, except that it holds a type instead of a value.
Every T in the signature refers to that one placeholder. That is the promise: what the array holds and what comes back are the same. any cannot state this.
Whoever writes the function does not know what T will be, and does not need to. Each call supplies its own arguments, and the compiler works out T for that call alone.
The compiler reads T = string from the first argument, then checks the second against it. If you want two independent types, declare two placeholders: pair<A, B>(a: A, b: B). There is a lab for that.
The name T is only a convention
T stands for Type. You can call it Item or Row instead, exactly as a value parameter can be called x or count. Other common short names are K and V for a key and a value, and E for an element. The names are short because the placeholder often really could be anything. When it does mean something specific, use a real name: Paginated<Order> reads better than Paginated<T> at a use site.
Filling the hole: inference first, explicit when needed
Most of the time you never see T being filled in. The compiler reads it from the arguments. Writing the type argument by hand is the exception, and it is worth knowing when it is required.
T is inferred as never and the result is undefined. This is not an error, but it is rarely what you wanted. Seeing never where you did not expect it is a good signal to write the type argument.When there is nothing at all to infer from
An empty array is still an argument, so the compiler has something to work with. The harder case is a type parameter that appears only in the return type. Then no argument mentions it, and the compiler falls back to unknown.
This shape is common in code that parses or fetches data. Be careful with it: parseJson promises to return a T, but nothing checks that the parsed text actually has that shape. The type argument is an assertion by the caller, not a guarantee by the compiler. Chapter 03 covers how to check such a value before trusting it.
The rule of thumb: let inference do the work. Write the type argument only when the arguments cannot supply one (an empty array, a parameterless call, a type parameter used only in the return type), or when the inferred type is not the one you want.
Constraints: not every type may fill the hole
A completely open placeholder has a cost: inside the function you can do almost nothing with it. extends narrows what may be passed in, and in return the body gets to use what is guaranteed.
extends here does not mean inheritance
T extends { length: number } reads as: T may be any type, as long as it is assignable to { length: number }. It does not say that T is a subclass of anything, and no class is involved. The test is the structural test from the previous chapter: does the type have a length property of type number? string does. number[] does. An anonymous { length: 12 } does.
One more thing the constraint does not do: it does not replace T with the constraint. longest("a", "b") returns string, not { length: number }. The constraint is only a condition on the argument. The placeholder still holds the full type that was passed in.
.length of two values, so its placeholder carries a condition: whatever fills it must have length: number. Pick a candidate above and send it through.T is the object. K is constrained to keyof T, which is the union of that object's key names. The constraint is what makes this safe: because K can only be a key that T really has, the indexed access T[K] is always a type that exists, and a wrong key name is rejected before the program runs. keyof and T[K] are covered properly in chapter 07.Generic types, interfaces, and classes
Functions are not the only place a placeholder can go. type, interface, and class can all take type parameters. Structures with a fixed outer shape and a varying inside are the usual reason to reach for one.
Paginated<Order> is a type; Paginated on its own is not.Basket<string> only ever accepts strings. On a generic method it is fixed once per call, so the same Counter instance can count numbers and then count strings.<T extends string> limits which types are allowed. <T = string> supplies a type when none is written and none can be inferred. They answer different questions, and <T extends string = "small"> uses both at once.You have been using generics all along
string[] is shorthand for Array<string>, and Array is a generic interface. Promise<Order> is a value that will be an Order later. Map<string, number> has two placeholders. Once you can read the brackets, the type signatures in the standard library become readable documentation.
One thing that surprises people: Array<Dog> is assignable to Array<Animal>, even though that lets you push a non-Dog into the original array. This is a deliberate decision about how the methods of Array are compared, not something generics do in general. Chapter 02 §04 works through it.
Generics do not exist at runtime, and three common mistakes
One last calibration: a type parameter is a compile-time thing only. Then three ideas that beginners often get wrong.
Type erasure applies to generics like everything else. <T>, <string>, and every annotation are removed during compilation. So there is no way to ask what T is while the program runs, and no way to write new T() or if (T === String). A generic function does not know its type argument while it runs. The compiler resolved that argument earlier, while it was checking the file. If you need to branch on a type at runtime, you need a real check on a value, which is chapter 03.
Mistake 1: a type parameter that is used only once
function log<T>(x: T): void { console.log(x) } declares T and then mentions it once. It links nothing to anything, so it promises nothing. A useful rule: a type parameter should appear at least twice — linking two parameters, or linking a parameter to the return type. If it appears once, (x: unknown) says the same thing more honestly.
Mistake 2: thinking a generic is just any
any gives up on the type: whatever goes in, what comes out is unchecked. A generic keeps the type: T is resolved to a concrete type at the call, and the input and output stay checked all the way through. The two work in opposite directions. They only look alike because both accept many types.
Mistake 3: <T> on an arrow function in a .tsx file
A plain .ts file does not have this problem, because it has no JSX syntax to be confused with. Neither does a function declaration, in any file.
Labs
Four tasks, all of which run in the TypeScript Playground: fold three copies into one generic, build a paginated container, try a constraint, and use two placeholders at once.
Quiz
Eight questions. After this chapter, a signature like <T extends X = Y> should read as: a placeholder, with a condition on it, and a value to use when none is given.
Which sentence describes best what generics are for?
Given function first<T>(arr: T[]): T | undefined, the call first([9.9, 19.9]) is written without angle brackets. What is T?
"A generic is just any with extra steps." What is the strongest reply?
Given function longest<T extends { length: number }>(a: T, b: T): T, which call is rejected?
Which of these statements about generics are true? (choose all)
A colleague wrote function log<T>(x: T): void { console.log(x) } and asks you to review it. What is the most useful comment?
To put a condition on a type parameter — "T may not be just anything, it has to be assignable to this shape" — you use the keyword ________.
Given function getProp<T, K extends keyof T>(obj: T, key: K): T[K] and const order = { item: "Boba milk tea", price: 18 }, what happens on getProp(order, "topping")?
- A type parameter is a placeholder for a type, filled in at the call site. Every
Tin one signature is the sameT, which is what keeps the output type tied to the input type. - Generics and
anypoint in opposite directions.anydrops the type at the door. A generic carries it from the input through to the output. - Inference is the default, and it reads the arguments. Write the type argument yourself when the arguments cannot supply one — an empty array gives
never, and a parameter used only in the return type givesunknown. extendsin a type parameter list is a constraint, not inheritance: T must be assignable to that shape. A default (<T = string>) is a separate thing, and the two can be combined.K extends keyof TwithT[K]is the standard way to read a property safely.- Type parameters are erased: after compilation there is no
Tto inspect, so a generic function does not know its type argument at runtime. A generic class fixes its placeholder per instance; a generic method fixes it per call.