TypeScript Fundamentals: Types, Generics and Real-World Patterns · Generics, Utility Types and Real-World TypeScript
Generics: Reusable, Type-Safe Code
Write functions, interfaces and classes that work with many types without losing type information, constrain type parameters with extends and keyof, and use defaults.
What you'll build
A generic Stack<T>, a pluck function that reads a property from a list of objects with full type safety, and a Result<T, E> type with defaults.
The idea
A generic is a type parameter: a placeholder for a type chosen by the caller. It lets one definition work for many types while keeping the relationship between input and output.
function first<T>(items: T[]): T | undefined { return items[0]; }
first([1, 2]) returns number | undefined; first(["a"]) returns string | undefined. Without generics you would use any and lose that information.
Useful tools:
- Inference: you rarely write
first<number>(...); TypeScript infersTfrom the arguments. - Constraints:
T extends { id: number }requires thatThas anid. Inside the function you can use.idsafely. keyof T: a union of the property names ofT.K extends keyof Tties a key to its object, soobj[key]has the exact right type.- Defaults:
interface Result<T, E = Error>lets callers omit the second type. - Generic classes and interfaces:
class Stack<T>,interface Repository<T>.
Generics are about relationships. If a type parameter appears only once in a signature, you probably don't need it.
The code
class Stack<T> {
private items: T[] = [];
push(item: T): this {
this.items.push(item);
return this;
}
pop(): T | undefined {
return this.items.pop();
}
get size(): number {
return this.items.length;
}
}
const numbers = new Stack<number>().push(1).push(2).push(3);
console.log(numbers.pop(), numbers.size);
// K extends keyof T ties the key to the object
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const people = [
{ id: 1, name: "Ada", age: 36 },
{ id: 2, name: "Grace", age: 45 },
];
const names = pluck(people, "name"); // string[]
const ages = pluck(people, "age"); // number[]
console.log(names, ages);
// @ts-expect-error: "email" is not a key of the person type
pluck(people, "email");
// A constraint: T must have an id
function indexById<T extends { id: number }>(items: T[]): Map<number, T> {
return new Map(items.map((item) => [item.id, item]));
}
console.log(indexById(people).get(2)?.name);
// A type parameter with a default
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function safeParse(text: string): Result<number> {
const value = Number(text);
return Number.isNaN(value) ? { ok: false, error: new Error(`not a number: ${text}`) } : { ok: true, value };
}
for (const input of ["42", "abc"]) {
const result = safeParse(input);
console.log(result.ok ? `value ${result.value}` : result.error.message);
}
Output
3 2 [ 'Ada', 'Grace' ] [ 36, 45 ] Grace value 42 not a number: abc
Common mistakes
- Adding a type parameter that is used once.
function log<T>(x: T): voidgains nothing over(x: unknown). - Forgetting constraints and then reaching for
any. If you need.idor.length, constrain withextends. - Passing type arguments you don't need. Let inference work; add
<...>only when it cannot infer (for examplenew Stack<number>()). - Over-engineering with deep generic chains. If readers can't tell what a signature means, simplify it.
- Confusing
Tthe type with a runtime value. Type parameters are erased; you cannot donew T()ortypeof T.
Key points
- Generics are type parameters that preserve relationships between inputs and outputs.
- Constrain with
extends; tie keys to objects withK extends keyof T. - Defaults (
E = Error) make generic types easier to use. - Prefer inference; only supply type arguments when the compiler cannot work them out.
Further reading: The Handbook: Generics.
Practice questions
What does `K extends keyof T` express in `function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]`?
Answer
That key must be one of the property names of T, and the return type is the array of that property's type, T[K][]. The key and the result type stay linked, so the compiler rejects unknown keys and knows the exact element type.
When is a generic type parameter unnecessary?
Answer
When it appears only once in the signature and doesn't relate two types. For example function log<T>(x: T) could just take unknown, because nothing connects T to another part of the signature.
Open in the app to mark lessons complete, take notes and track your progress.