Skip to content

Latest commit

 

History

History
180 lines (133 loc) · 8.26 KB

functions-on-types.md

File metadata and controls

180 lines (133 loc) · 8.26 KB

Item 50: Think of Generics as Functions Between Types

Things to Remember

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

Code Samples

type MyPartial<T> = {[K in keyof T]?: T[K]};

💻 playground


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


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


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

💻 playground


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


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


/**
 * 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


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


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

💻 playground


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

💻 playground


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

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

💻 playground


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

💻 playground