Generics
Write code once for every type it can hold, without giving up the checks: generic functions whose output type follows their input, keyof and indexed access types, and a generic repository constrained to things with an id, the shape of every repository in NestJS.
What you'll learn
- Write generic functions whose return type follows from their arguments
- Constrain a type parameter with extends, and use keyof and T[K] to type a key and its value
- Write a generic class and use it for different types
A function that returns the first item of a list works the same for numbers, cats and owners. Written with any, it works for all of them and checks none: first(cats).nmae compiles, and fails at runtime. Written once per type, it is three copies of the same code. Generics are the third way: code written once, with a type left open as a parameter, filled in wherever it is used. Every repository in NestJS is Repository<Cat> or Repository<Owner>, one class with its item type as a parameter, and that is where this lesson ends up.
Type parameters
A type parameter is declared in angle brackets and then used like any other type:
function last<T>(items: readonly T[]): T | undefined {
return items[items.length - 1];
}
last([1, 2, 3]); // T is number, so the result is number | undefined
last(['a', 'b']); // T is string
The caller rarely writes T: TypeScript infers it from the arguments, and the return type follows. That is the whole point: the output's type is tied to the input's, where any would cut the tie.
Constraints
Sometimes the code needs something from T. A function that reads .id cannot accept every type, so the parameter is constrained with extends:
function ids<T extends { id: number }>(items: readonly T[]): number[] {
return items.map((item) => item.id);
}
Any type with a numeric id fits; ids(['a']) is a compile error at the call.
keyof and indexed access
keyof T is the union of T's property names, and T[K] is the type of the property named K:
type Owner = { id: number; email: string };
type OwnerKey = keyof Owner; // 'id' | 'email'
type Email = Owner['email']; // string
Together they type a function that reads a property by name, with the key checked and the value's type known:
function sortBy<T, K extends keyof T>(items: readonly T[], key: K): T[] {
return [...items].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0));
}
sortBy(owners, 'email'); // fine
sortBy(owners, 'mail'); // error: 'mail' is not a key of Owner
K extends keyof T reads as "K is one of T's keys". Because K is its own parameter, the compiler knows which key was passed, not just "some key", so a second argument can be typed T[K], the value at exactly that key.
Generic classes
A class can have type parameters too, available to all its members:
class Cache<K, V> {
private readonly entries = new Map<K, V>();
set(key: K, value: V): void { this.entries.set(key, value); }
get(key: K): V | undefined { return this.entries.get(key); }
}
const sessions = new Cache<string, { userId: number }>();
Each new Cache<…> is its own instance with its own types; nothing leaks between them. A Map is a good store for items keyed by id: lookups are direct, set on an existing key replaces the value, and iteration follows the order keys were first inserted.
Checking generic types from a test
A generic function's type depends on how it is called, so the spec checks it at a particular instantiation. typeof first<number> is first with T fixed to number, and ReturnType of that must be exactly number | undefined. A version that returns any or unknown fails that check, even if every runtime test passes.
Your task
The work is in repository.ts.
first(items)is generic: it returns the first item, typed as the list's item type orundefined.pluck(items, key)is generic over the item type and one of its keys:pluck(cats, 'name')is astring[], and a key the items lack is a compile error.Repositorytakes a type parameter that must have a numericid.save(item)stores it under its id, replacing any item with the same id, and returns it;findById(id)returns the item orundefined;findBy(key, value)returns every item whosekeyhas thatvalue, with the value typed by the key;all()returns every item in the order it was first saved.
When it fails
Type 'false' does not satisfy the constraint 'true'onFirstOfNumbers:firstreturnsunknownoranyrather thanT | undefined.Type 'Repository' is not generic: the class has no type parameter yet.Unused '@ts-expect-error' directivebesidefindBy('age', '3'): the value is typedunknown, so anything is accepted. Type itT[K]withK extends keyof T.- "replaces an item saved again" finds four items: the repository stores a list and appends. Key the items by id.
Remember
- A type parameter is a type left open, filled in (usually inferred) where the code is used.
extendsconstrains what a type parameter may be.keyof Tis T's keys;T[K]is the type at key K.- A generic class carries its parameter to every member, and each instance has its own.
Stuck? Show a hint
A type parameter goes in angle brackets after the name: function first<T>(items: readonly T[]): T | undefined. For a key of T, add a second parameter constrained to keyof T, and the value at that key has the type T[K]. class Repository<T extends Entity> makes every T have an id; a Map<number, T> keeps the items by id, in the order they were first set.