TypeScript Tips and Tricks

Image of Author
Invalid Date (last updated February 2, 2023)

Type Predicates and Type Narrowing

aka, The Problem of Sticky Union Types

Let's say you have a list of (string | number)[] and want to filter out the numbers to be left with string[]. The following intuitive sequence won't work.

const a = [1, 'two', 3, 'four']

const isString = (x): boolean => typeof x === 'string'
const b = a.filter(isString)
// b: (string | number)[] = ['two', 'four']

The reason this does not work is because the filter function only goes from same-type to same-type. It will not introspect your type and infer that it can safely augment it. Instead it will keep your union type as is.

What you likely want is to leverage a type predicate for the purpose of type narrowing. The only change in what follows is the return type of isString was changed from boolean to the type predicate x is string.

const a = [1, 'two', 3, 'four']

const isString = (x): x is string => typeof x === 'string'
const b = a.filter(isString)
// b: string[] = ['two', 'four']

Another example is a (T | null)[] where you want to remove the nulls.

const isNotNull = <T>(x: T | null): x is T => !!x
const a = [t1, null, t2].filter(isNotNull)
// a: T[] = [t1, t2]

Accessing deeply nested types

You can access deeply nested implicit types. For example, the type of a nested array:

interface A {
  list: { a: string; b: number }[]
}

function fun(item: A['list'][number]) {
  item.a
  item.b
}

// or

type B = A['list'][number]

Also, you don't even need a type

const A = {
  list: [
    { a: 'wow', b: 4 }
  ]
}

function fun(item: typeof A['list'][number]) {
  item.a
  item.b
}

type B = typeof A['list'][number]