Somewhere around 2019 you opened a TypeScript file, saw a function signature wearing angle
brackets like <T>, and closed the tab. Fair. It reads like the language showing off. Here is the
twist: you already understood the idea underneath it, you just never called it that. A function
takes an argument and does something with whatever value shows up. A generic function takes an
argument too, except the argument is a type, and it does something with whatever type shows up.
Same trick, one level up.
A generic is a function argument, for types
Watch what changes and what does not. The logic is identical on both sides, grab the last item out of an array. JavaScript never cared what was in the array. This TS version is written so it doesn't have to start caring either.
The ES6 you write
function latestOf(items) {
return items[items.length - 1]
}The TS version
function latestOf<T>(items: T[]): T {
return items[items.length - 1]
}What changed
items goes from a bare parameter to items: T[], an array of some type T nobody has picked yet. The return type, : T, promises whatever type went in comes back out, unchanged. T is not a real type like string or number, it is a placeholder, and TypeScript fills it in from whatever you actually pass at the call site, the exact same way a value parameter gets filled in from whatever value you actually pass.
That <T> right after the function name is where the type parameter gets declared, the same way
(items) is where the value parameter gets declared. Everything after it, items: T[] and : T,
just uses T like any other type name from that point on.
One function beats three copy-pasted ones
Here is the problem generics actually solve. Without them, keeping full type safety for a function that needs to work on more than one kind of array means writing it more than once:
function latestString(items: string[]): string {
return items[items.length - 1]
}
function latestNumber(items: number[]): number {
return items[items.length - 1]
}Same body, twice, with only the type changed. That is a maintenance problem waiting to happen: fix
a bug in one and forget the other exists. latestOf<T> from above replaces both, and every future
one you would have needed:
latestOf(['Chelsie', 'Angela']) // T becomes string, inferred, no annotation written
latestOf([7, 9]) // T becomes number, inferred, same functionNobody writes latestOf<string>(...) by hand here. TypeScript looks at the array you actually
passed, sees it is full of strings, and fills in T with string for that call, the same way it
already infers the type of a const from its initializer. One function, every call site still
fully typed, no duplication.
Two type parameters, same idea
A function can take more than one type argument, same as it can take more than one value argument. Comma-separate them inside the angle brackets:
function matchup<A, B>(nominee: A, vote: B): [A, B] {
return [nominee, vote]
}
matchup('Chelsie', 3) // A becomes string, B becomes number, inferred from the callA and B are just names, same status as T, nothing magic about the letter. Each one gets
filled in independently from whatever you pass in that argument's position, and the return type
[A, B] keeps both of them attached to the result, in order.
Being honest about empty arrays
One catch worth seeing on purpose. items[items.length - 1] on an empty array does not throw,
it quietly hands back undefined, because that is how array indexing has always worked in
JavaScript. Plain strict mode trusts you that the array is non-empty and will happily type that
expression as bare T, no warning. This site's own build settings turn on an extra flag,
noUncheckedIndexedAccess, which makes TypeScript stop trusting that assumption and type every
indexed access as T | undefined automatically, index included. Whether or not that flag is on
in a given project, the honest move is the same either way: annotate a generic function's return
type as T | undefined yourself whenever an empty array is possible, so every caller sees the
real risk in the type, not just in a comment.
That is the whole lesson. Now prove it in the comp below.