Lesson 7 of 1012 min
Generics
Write a function or type once and use it with many types, without giving up type safety. Generics are type parameters.
In this lesson
- Explain what a type parameter is
- Write generic functions and types
- Constrain generics with extends and keyof
The problem generics solve#
Imagine a function that returns the first item of an array. Without generics you either write one per type or give up type information.
function firstAny(items: unknown[]): unknown { return items[0];}const value = firstAny(["a", "b"]);1value is unknown: TypeScript forgot that the array held strings, so you cannot call string methods without checking first.
Type parameters#
A generic function declares a type parameter in angle brackets. TypeScript fills it in from the arguments.
function first<T>(items: T[]): T | undefined {1 return items[0];}const word = first(["a", "b"]);2const num = first([10, 20]);3console.log(word?.toUpperCase(), num?.toFixed(2));T is a placeholder for "whatever type the array holds". The return type says it may be undefined for an empty array.
Here T is inferred as string, so word is string | undefined.
Here T is number. Same function, still fully typed.
Generic types#
Types can take parameters too. A result type that carries either data or an error is a classic example, and you will use it in the next lesson.
type Result<T> = { ok: true; value: T } | { ok: false; error: string };function parseAge(input: string): Result<number> { const age = Number(input); if (!Number.isInteger(age) || age < 0) return { ok: false, error: `"${input}" is not an age` }; return { ok: true, value: age };}const result = parseAge("29");if (result.ok) console.log(result.value + 1); // 30else console.log(result.error);Arrays are generic as well: string[] is shorthand for Array<string>, and Promise<User> is a promise that resolves to a User.
Constraints#
Sometimes a type parameter must have certain features. extends sets a minimum requirement.
function longest<T extends { length: number }>(a: T, b: T): T {1 return a.length >= b.length ? a : b;}console.log(longest("kopi", "teh")); // "kopi"2console.log(longest([1, 2], [1, 2, 3])); // [1, 2, 3]Any T is allowed as long as it has a numeric length: strings and arrays both qualify.
Passing a number would be an error, because numbers have no length.
keyof: safe property access#
keyof T is the union of a type's property names. Combined with a constraint, it lets you write a property getter that cannot ask for a field that does not exist.
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {1 return items.map((item) => item[key]);}const people = [ { name: "Rina", age: 31 }, { name: "Dewa", age: 27 },];const names = pluck(people, "name"); // string[]const ages = pluck(people, "age"); // number[]console.log(names, ages);K must be one of T's keys, and T[K] is the type of that property. pluck(people, "email") would not compile.
When not to use generics
If a type parameter appears only once, it adds nothing: function log<T>(value: T): void is just function log(value: unknown): void. Use a generic when it connects types, such as an input to an output. Over-generic code is harder to read than a little duplication.
ExerciseA typed last()
Write last<T>(items: T[]): T | undefined that returns the final element. Then call it with an array of booleans and check the inferred type.
Show solution
function last<T>(items: T[]): T | undefined { return items[items.length - 1];}const flag = last([true, false, true]); // boolean | undefinedconsole.log(flag);Recap#
- Type parameters let one function or type work with many types while staying precise.
- TypeScript usually infers them from arguments.
- Constrain with
extends; usekeyofandT[K]for safe property access.