Skip to content

Latest commit

 

History

History
437 lines (336 loc) · 15.8 KB

map-between-types.md

File metadata and controls

437 lines (336 loc) · 15.8 KB

Item 15: Use Type Operations and Generic Types to Avoid Repeating Yourself

Things to Remember

  • The DRY (don't repeat yourself) principle applies to types as much as it applies to logic.
  • Name types rather than repeating them. Use extends to avoid repeating fields in interfaces.

[role="less_space pagebreak-before"]

  • Build an understanding of the tools provided by TypeScript to map between types. These include keyof, typeof, indexing, and mapped types.
  • Generic types are the equivalent of functions for types. Use them to map between types instead of repeating type-level operations.
  • Familiarize yourself with generic types defined in the standard library, such as Pick, Partial, and ReturnType.
  • Avoid over-application of DRY: make sure the properties and types you're sharing are really the same thing.

Code Samples

console.log(
  'Cylinder r=1 × h=1',
  'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 1 * 1,
  'Volume:', 3.14159 * 1 * 1 * 1
);
console.log(
  'Cylinder r=1 × h=2',
  'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 2 * 1,
  'Volume:', 3.14159 * 1 * 2 * 1
);
console.log(
  'Cylinder r=2 × h=1',
  'Surface area:', 6.283185 * 2 * 1 + 6.283185 * 2 * 1,
  'Volume:', 3.14159 * 2 * 2 * 1
);

💻 playground


type CylinderFn = (r: number, h: number) => number;
const surfaceArea: CylinderFn = (r, h) => 2 * Math.PI * r * (r + h);
const volume: CylinderFn = (r, h) => Math.PI * r * r * h;

for (const [r, h] of [[1, 1], [1, 2], [2, 1]]) {
  console.log(
    `Cylinder r=${r} × h=${h}`,
    `Surface area: ${surfaceArea(r, h)}`,
    `Volume: ${volume(r, h)}`);
}

💻 playground


interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;
  lastName: string;
  birth: Date;
}

💻 playground


function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}

💻 playground


interface Point2D {
  x: number;
  y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }

💻 playground


function get(url: string, opts: Options): Promise<Response> { /* ... */ }
function post(url: string, opts: Options): Promise<Response> { /* ... */ }

💻 playground


type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

💻 playground


interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}

💻 playground


interface Bird {
  wingspanCm: number;
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
}
interface Mammal {
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
  eatsGardenPlants: boolean;
}

💻 playground


interface Vertebrate {
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
}
interface Bird extends Vertebrate {
  wingspanCm: number;
}
interface Mammal extends Vertebrate {
  eatsGardenPlants: boolean;
}

💻 playground


type PersonWithBirthDate = Person & { birth: Date };

💻 playground


interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  // omits pageContents
}

💻 playground


interface TopNavState {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
};

💻 playground


type TopNavState = {
  [K in 'userId' | 'pageTitle' | 'recentFiles']: State[K]
};

💻 playground


type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

💻 playground


interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';  // Repeated types!

💻 playground


type ActionType = Action['type'];
//   ^? type ActionType = "save" | "load"

💻 playground


type ActionRecord = Pick<Action, 'type'>;
//   ^? type ActionRecord = { type: "save" | "load"; }

💻 playground


interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
interface OptionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: OptionsUpdate) { /* ... */ }
}

💻 playground


type OptionsUpdate = {[k in keyof Options]?: Options[k]};

💻 playground


type OptionsKeys = keyof Options;
//   ^? type OptionsKeys = keyof Options
//      (equivalent to "width" | "height" | "color" | "label")

💻 playground


class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: Partial<Options>) { /* ... */ }
}

💻 playground


interface ShortToLong {
  q: 'search';
  n: 'numberOfResults';
}
type LongToShort = { [k in keyof ShortToLong as ShortToLong[k]]: k };
//   ^? type LongToShort = { search: "q"; numberOfResults: "n"; }

💻 playground


interface Customer {
  /** How the customer would like to be addressed. */
  title?: string;
  /** Complete name as entered in the system. */
  readonly name: string;
}

type PickTitle = Pick<Customer, 'title'>;
//   ^? type PickTitle = { title?: string; }
type PickName = Pick<Customer, 'name'>;
//   ^? type PickName = { readonly name: string; }
type ManualName = { [K in 'name']: Customer[K]; };
//   ^? type ManualName = { name: string; }

💻 playground


type PartialNumber = Partial<number>;
//   ^? type PartialNumber = number

💻 playground


const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: '#00FF00',
  label: 'VGA',
};
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

💻 playground


type Options = typeof INIT_OPTIONS;

💻 playground


function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  };
}
// Return type inferred as { userId: string; name: string; age: number, ... }

💻 playground


type UserInfo = ReturnType<typeof getUserInfo>;

💻 playground


interface Product {
  id: number;
  name: string;
  priceDollars: number;
}
interface Customer {
  id: number;
  name: string;
  address: string;
}

💻 playground


// Don't do this!
interface NamedAndIdentified {
  id: number;
  name: string;
}
interface Product extends NamedAndIdentified {
  priceDollars: number;
}
interface Customer extends NamedAndIdentified {
  address: string;
}

💻 playground