Item 50: Think of Generics as Functions Between Types
May 10, 2024 ยท View on GitHub
Things to Remember
- Think of generic types as functions between types.
- Use
extendsto constrain the domain of type parameters, just as you'd use a type annotation to constrain a function parameter. - Choose type parameter names that increase the legibility of your code, and write TSDoc for them.
- Think of generic functions and classes as conceptually defining generic types that are conducive to type inference.
Code Samples
type MyPartial<T> = {[K in keyof T]?: T[K]};
interface Person {
name: string;
age: number;
}
type MyPartPerson = MyPartial<Person>;
// ^? type MyPartPerson = { name?: string; age?: number; }
type PartPerson = Partial<Person>;
// ^? type PartPerson = { name?: string; age?: number; }
type MyPick<T, K> = {
[P in K]: T[P]
// ~ Type 'K' is not assignable to type 'string | number | symbol'.
// ~~~~ Type 'P' cannot be used to index type 'T'.
};
// @ts-expect-error (don't do this!)
type MyPick<T, K> = { [P in K]: T[P] };
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
type FirstNameOnly = MyPick<Person, 'firstName'>;
// ^? type FirstNameOnly = { firstName: unknown; }
type Flip = MyPick<'age', Person>;
// ^? type Flip = {}
type MyPick<T, K> = { [P in K & PropertyKey]: T[P & keyof T] };
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
type FirstNameOnly = MyPick<Person, 'firstName'>;
// ^? type FirstNameOnly = { firstName: never; }
type MyPick<T extends object, K extends keyof T> = {[P in K]: T[P]};
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
type FirstNameOnly = MyPick<Person, 'firstName'>;
// ~~~~~~~~~~~
// Type '"firstName"' does not satisfy the constraint 'keyof Person'.
type Flip = MyPick<'age', Person>;
// ~~~~~ Type 'string' does not satisfy the constraint 'object'.
/**
* Construct a new object type using a subset of the properties of another one
* (same as the built-in `Pick` type).
* @template T The original object type
* @template K The keys to pick, typically a union of string literal types.
*/
type MyPick<T extends object, K extends keyof T> = {
[P in K]: T[P]
};
function pick<T extends object, K extends keyof T>(
obj: T, ...keys: K[]
): Pick<T, K> {
const picked: Partial<Pick<T, K>> = {};
for (const k of keys) {
picked[k] = obj[k];
}
return picked as Pick<T, K>;
}
const p: Person = { name: 'Matilda', age: 5.5 };
const age = pick(p, 'age');
// ^? const age: Pick<Person, "age">
console.log(age); // logs { age: 5.5 }
type P = typeof pick;
// ^? type P = <T extends object, K extends keyof T>(
// obj: T, ...keys: K[]
// ) => Pick<T, K>
const age = pick<Person, 'age'>(p, 'age');
// ^? const age: Pick<Person, "age">
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const dateBox = new Box(new Date());
// ^? const dateBox: Box<Date>
type MapValues<T extends object, F> = {
[K in keyof T]: F<T[K]>;
// ~~~~~~~ Type 'F' is not generic.
};