Item 50: Think of Generics as Functions Between Types
要点
- Think of generic types as functions between types.
- Use
extends
to 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.
- 把泛型类型看作是类型之间的函数。
- 使用
extends
来约束类型参数的领域,就像你用类型注解来约束函数参数一样。 - 选择能增加代码可读性的类型参数名称,并为它们编写 TSDoc 文档。
- 把泛型函数和类看作是概念上定义了有利于类型推断的泛型类型。
正文
ts
type MyPartial<T> = { [K in keyof T]?: T[K] }
ts
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; }
ts
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
// @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; }
ts
type FirstNameOnly = MyPick<Person, 'firstName'>
// ^? type FirstNameOnly = { firstName: unknown; }
type Flip = MyPick<'age', Person>
// ^? type Flip = {}
ts
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; }
ts
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'.
ts
/**
* 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]
}
ts
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 }
ts
type P = typeof pick
// ^? type P = <T extends object, K extends keyof T>(
// obj: T, ...keys: K[]
// ) => Pick<T, K>
ts
const age = pick<Person, 'age'>(p, 'age')
// ^? const age: Pick<Person, "age">
ts
class Box<T> {
value: T
constructor(value: T) {
this.value = value
}
}
const dateBox = new Box(new Date())
// ^? const dateBox: Box<Date>
ts
type MapValues<T extends object, F> = {
[K in keyof T]: F<T[K]>
// ~~~~~~~ Type 'F' is not generic.
}