TypeScript lesson 8 of 9
Generics Basics
Why writing a function against any throws away type safety, how a generic type parameter lets one function work for every type while TypeScript still checks it, and how to write a simple generic constraint.
Published · Every example on this page was run before it was published.
A function that returns the first item of a list, or swaps two values, or wraps a value in a small container, does not care what type of value it is handling - the logic is identical whether the list holds numbers, strings, or anything else. Writing that function once, in a way that works for every type while TypeScript still checks every call against the type actually used, is exactly what generics are for.
The Problem: any Throws the Type Away
The obvious way to write a function that accepts "anything" is to type its parameter any. That does
make it reusable, but at a real cost: any opts a value out of type checking entirely, for the rest of
wherever it flows.
function firstElementAny(items: any[]): any {
return items[0];
}
const first = firstElementAny([1, 2, 3]);
console.log(first.toUpperCase());This compiles without a single complaint, because any tells the compiler to stop checking - first is
any, and any is considered to have every method, whether or not it actually does. It only fails once
it actually runs, when calling .toUpperCase() on the number 1 crashes. The type system was supposed
to catch exactly this kind of mistake, and any switched it off.
The Fix: A Generic Type Parameter
A generic type parameter, written in angle brackets right after the function name, is a placeholder for "whatever type this particular call is using" - filled in fresh at every call site, and tracked properly the whole way through:
function firstElement<T>(items: T[]): T {
return items[0];
}
const first = firstElement([1, 2, 3]);
console.log(first.toUpperCase());main.ts(6,19): error TS2339: Property 'toUpperCase' does not exist on type 'number'.Nothing about the function's logic changed - it is the same one line, returning the first item. What
changed is that TypeScript now knows exactly what T is for this call: it looked at the argument,
[1, 2, 3], saw every element is a number, and inferred T as number for this call alone. first
is therefore typed number, and the mistake that crashed the any version is caught here before the
program ever runs.
Calling With an Explicit Type Argument
Most of the time TypeScript infers T from the arguments you pass, the way it just did. You can also
state it explicitly, using the same angle-bracket syntax at the call site - useful when there is nothing
in the arguments alone to infer it from:
function firstElement<T>(items: T[]): T {
return items[0];
}
console.log(firstElement<string>(["red", "green", "blue"]));
console.log(firstElement([10, 20, 30]));red
10The first call spells out <string> explicitly; the second leaves it to inference from [10, 20, 30].
Both are checked exactly the same way - only how T was decided differs.
More Than One Type Parameter
A function can take several independent generic type parameters, each inferred separately from a different argument:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
console.log(pair("age", 30));
console.log(pair(true, "yes"));[ 'age', 30 ]
[ true, 'yes' ]On the first call, A is inferred as string and B as number, purely from the two arguments
actually passed - pair's definition never mentions either concrete type.
Generic Constraints
Sometimes a generic function needs to rely on something about T, without pinning it down to one
concrete type. Writing <T extends ...> lets you require T to have at least certain properties, while
still leaving it open to every type that has them:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest("hello", "hi"));
console.log(longest([1, 2, 3], [1, 2]));hello
[ 1, 2, 3 ]T extends { length: number } accepts any type that has a numeric length property - strings and
arrays both qualify, which is why longest works on either without being written twice.
Common Mistakes
Accessing a property a bare, unconstrained T does not guarantee exists. Without an extends
clause, T could be absolutely anything, so the compiler will not let you assume even something as
common as .length:
function printLength<T>(value: T): void {
console.log(value.length);
}main.ts(2,21): error TS2339: Property 'length' does not exist on type 'T'.The fix is the constraint from the previous section: function printLength<T extends { length: number }>(value: T)
tells the compiler exactly what it is allowed to assume about T, and the property access then
type-checks.
Reaching for a generic where one concrete type would read more clearly. Generics earn their keep
when a type genuinely needs to flow from a function's input through to its output, the way T does in
firstElement and A/B do in pair. A function that will only ever be called with numbers gains
nothing from <T> - it just makes the signature harder to read for no benefit, when function double(n: number): number
already says everything that needs saying.
Assuming a generic function "remembers" the type from an earlier call. Every call to a generic function infers its own type parameters independently - nothing is shared or carried over between calls, even calls to the exact same function:
function identity<T>(value: T): T {
return value;
}
console.log(identity(42));
console.log(identity("forty-two"));42
forty-twoThe first call infers T as number; the second, on the very next line, infers it fresh as string.
Neither call has any effect on how the other one is checked.
Next Steps
Many real implementations of binary-search are written generically, so the array can hold numbers,
strings, or anything comparable - a good next step for seeing <T> used in something less abstract than
a firstElement helper.
In the TypeScript playground, write your own small generic function -
something like last<T>(items: T[]): T - and call it with an array of numbers and then an array of
strings, checking what each call infers T to be. Then remove the <T> and replace it with any
everywhere it appeared, and try to reproduce the kind of mistake this lesson opened with.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.