Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.

Down the rabbit hole with typescript's mapped and lookup types

December 14, 2018

Two of the more abstract concepts in typescript are mapped types and lookup types.

Mapped Types

Mapped types provide a mechanism to create new types from existing types.

For example if we have an address type:

type Address = {
  houseNumber: number
  street: string
  town: string
  postCode: string
}

The above type specifies that all fields are required. But if a new requirement states that an address does not have any required fields then Typescript comes with a built in Partial type that can transform all the fields to optional:

/**
 * Make all properties in T optional
 */
type Partial<T> = { [P in keyof T]?: T[P] }

We can then make a new OptionalAddress type from the existing Address:

type OptionalAddress = Partial<Address>

This saves us having to copy and paste the existing AddressType and then redefining its fields to be optional like this:

type Address = {
  houseNumber?: number
  street?: string
  town?: string
  postCode?: string
}

Integral in this mapping process it the keyof operator that yields the permitted property names for type T. The keyof operator returns a union of string literals and is the same as explicitly stating each key:

type Address = {
  houseNumber: number
  street: string
  town: string
  postCode: string
}

// get all the prop names of Address
// rather than duplicating the key names
// type AddressPropNames = 'houseNumber' | 'street' | 'town' | 'postcode'
// we use keyof to map the field names
type AddressPropNames = keyof Address // 'houseNumber' | 'street' | 'town' | 'postcode'

Lookup types

THe keyof operator is actually called an index type query. It’s like a query for keys on object types, the same way that typeof can be used to query for types on values.

The dual of this is indexed access types called lookup types. Syntactically, they look exactly like element access, but are written as types:

type T1 = Address[houseNumber] // number
type T2 = Address[street] // string

We can also get all the field types of Address

type AddressTypes = Address[AddressPropsNames] // 'number' | 'string' | 'string' | 'string'

We can even do this:

type T3 = string['charAt'] // (pos: number) => string
type T4 = string[]['push'] // (...items: string[]) => number
type T5 = string[][0] // string

Why is this useful?

I came across a situation recently where I needed to give types to an object with 4 very similar functions.

export interface Coordinate {
  x: number
  y: number
}

export interface Line {
  source: Coordinate
  target: Coordinate
}

const ScaleFactor = 7

const Zoom: any = {
  x(data: any) {
    return data.x <= 0 ? 0 : data.x + ScaleFactor
  },

  y(data: any {
    return data.y === 0 ? 0 : data.x + ScaleFactor
  },

  source(data: any {
    return { x: x(data.source.x), y: y(data.source.y) }
  },

  target(data: any {
    return { x: x(data.target.x), y: y(data.target.y) }
  },
}

The object Zoom has 4 very similar functions x, y, source and target. They all take one argument that is an object with one property and they all return a type.

I could have defined them all individually like this:

type Zoomable = {
  x: (data: { x: number }) => number
  y: (data: { y: number }) => number
  source: (data: { source: Coordinate }) => Coordinate
  target: (data: { target: Coordinate }) => Coordinate
}

There is a lot of duplication here so I refactored and my first attempt looked like this;

const ScaleFactor = 7

export interface Coordinate {
  x: number
  y: number
}

export interface Line {
  source: Coordinate
  target: Coordinate
}

export type CoordinateSelector<T extends keyof Coordinate> = (
  d: Pick<Coordinate, T>
) => typeof d[T]

export type LineSelector<T extends keyof Line> = (
  d: Pick<Line, T>
) => typeof d[T]

export type Zoomable = {
  x: CoordinateSelector<'x'>
  y: CoordinateSelector<'y'>
  source: LineSelector<'source'>
  target: LineSelector<'target'>
}

const Zoomable: Zoomable = {
  x(data) {
    return data.x <= 0 ? 0 : data.x + ScaleFactor
  },

  y(data) {
    return data.y <= 0 ? 0 : data.y + ScaleFactor
  },

  source(data) {
    return { x: this.x(data.source), y: this.y(data.source) }
  },

  target(data) {
    return { x: this.x(data.target), y: this.y(data.target) }
  },
}

If we look at one of the properties in isolation:

export type CoordinateSelector<T extends keyof Coordinate> = (
  d: Pick<Coordinate, T>
) => typeof d[T]

export type Zoomable = {
  x: CoordinateSelector<'x'>
}

const Zoomable: Zoomable = {
  x(data) {
    return data.x <= 0 ? 0 : data.x + ScaleFactor
  },
}

On line 6, I pass the type argument 'x' to CoordinateSelector which satisfies the type argument’s generic constraint T extends keyof Coordinate bceause x is a key of Coordinate.

keyof Coordinate on line 1 yields the type of permitted property names for Coordinate which are x and y. A keyof generic argument T is considered a subtype of string which allows us to pass type arguments like string literals, such as 'x' and 'y'.

d: Pick<Coordinate, T> on line 2 becomes d: Pick<Coordinate, 'x'> in this instance which allows us to pass any object as long as it has an x key.

And the return type => typeof d[T] becomes => typeof d['x'] which becomes => number.

Here is a typescript playground with the above. Try playing about with the types to break them and recorrect them.

This is really not a lot better than before and I might actually have more code, so I had another attempt and came of this:

const ScaleFactor = 7

export interface Coordinate {
  x: number
  y: number
}

export interface Line {
  source: Coordinate
  target: Coordinate
}

export type Selector<T, K extends keyof T> = (d: Pick<T, K>) => T[K]

export type CoordinateSelectors = {
  [P in keyof Coordinate]?: Selector<Coordinate, P>
}

export type LineSelectors = { [P in keyof Line]?: Selector<Line, P> }

export type Zoomable = CoordinateSelectors & LineSelectors

const Zoomable: Zoomable = {
  x(data) {
    return data.x <= 0 ? 0 : data.x + ScaleFactor
  },

  y(data) {
    return data.y <= 0 ? 0 : data.y + ScaleFactor
  },

  source(data) {
    return { x: this.x(data.source), y: this.y(data.source) }
  },

  target(data) {
    return { x: this.x(data.target), y: this.y(data.target) }
  },
}

This is much better, I have a generic mapped type Selector on line 13:

export type Selector<T, K extends keyof T> = (d: Pick<T, K>) => T[K]

Selector will iterate over the keys of T and create types for every key in T.

I then create 2 more specialised types, CoordinateSelectors on line 19 and LineSelectors on line 21:

export type CoordinateSelectors = {
  [P in keyof Coordinate]?: Selector<Coordinate, P>
}

export type LineSelectors = { [P in keyof Line]?: Selector<Line, P> }

CoordinateSelectors and LineSelectors do the same job as:

export type Zoomable = {
  x: CoordinateSelector<'x'>
  y: CoordinateSelector<'y'>
  source: LineSelector<'source'>
  target: LineSelector<'target'>
}

We have effectively a chain, starting with this:

export type CoordinateSelectors = {
  [P in keyof Coordinate]?: Selector<Coordinate, P>
}

This will map the keys of Coordinate which are x and y by applying the Selector<Coordinate, P> typing.

You could think of them as almost like function calls but generic type arguments are passed like this:

  x: CoordinateSelector<'x'>,
  y: CoordinateSelector<'y'>,

The new type is much terser but is maybe not as readable:

export type Selector<T, K extends keyof T> = (d: Pick<T, K>) => T[K]

export type CoordinateSelectors = {
  [P in keyof Coordinate]?: Selector<Coordinate, P>
}

export type LineSelectors = { [P in keyof Line]?: Selector<Line, P> }

export type Zoomable = CoordinateSelectors & LineSelectors

Here is a typescript playground of the above.

Feel free to comment on any of the above below.


Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.