Skip to content

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] }

💻 playground


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; }

💻 playground


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'.
}

💻 playground


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; }

💻 playground


ts
type FirstNameOnly = MyPick<Person, 'firstName'>
//   ^? type FirstNameOnly = { firstName: unknown; }
type Flip = MyPick<'age', Person>
//   ^? type Flip = {}

💻 playground


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; }

💻 playground


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'.

💻 playground


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]
}

💻 playground


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 }

💻 playground


ts
type P = typeof pick
//   ^? type P = <T extends object, K extends keyof T>(
//         obj: T, ...keys: K[]
//      ) => Pick<T, K>

💻 playground


ts
const age = pick<Person, 'age'>(p, 'age')
//    ^? const age: Pick<Person, "age">

💻 playground


ts
class Box<T> {
  value: T
  constructor(value: T) {
    this.value = value
  }
}

const dateBox = new Box(new Date())
//    ^? const dateBox: Box<Date>

💻 playground


ts
type MapValues<T extends object, F> = {
  [K in keyof T]: F<T[K]>
  //              ~~~~~~~ Type 'F' is not generic.
}

💻 playground

Released under the MIT License.