Narrow Union on Nested Value


Here's how to narrow a TypeScript union type on a nested value in an object.

Type Narrowing

Narrowing a type means we start with a more-general super type and determine which more-specific sub type we're actually dealing with.

In this case, we want to know if something is either a magazine or a book. Both are readable.

More on narrowing a type here.

Nested Object

What do we mean by "nested"? It means that the type we're trying to narrow is a field on a larger object.

In this case, we have a person, le pomme de terre, who has something he is reading. Is it a magazine or a book? Let's find out!

Union Types

Here are the basic types, with Readable defined as a discriminated union.

interface ChristmasCouchPotato {
  reading: {
      __typename: 'Magazine'
      issueNumber: number
  } | {
    __typename: 'Book'
  }
}

type Readable = ChristmasCouchPotato['reading']

So, a potato instance might have this data:

{ 
  reading: {
    __typename: 'Magazine',
    issueNumber: 3
  }
}

Type the Discrimination

We have a ChristmasCouchPotato defined, but now we want to write code that just wants to deal in magazines, so we need to define a magazine reader.

We have extracted a Readable type from ChristmasCouchPotato. How could we define a MagazinePotato? We could rewrite ChristmasCouchPotato itself, but let's say that our system removes this possibility. (This happened to me in real life when the type was defined by a Graphql schema and the types were code generated.)

Here's how we would define a MagazinePotato:

type MagazinePotato = ChristmasCouchPotato & {
  reading: Extract<Readable, { __typename: 'Magazine' }>
}

What's going on here?

The Extract type is a utility that defines a new type in terms of the 2nd generic argument (what the docs call the "assigned Union"). Thus, we have produced a new type that acts like a filter, matching readables where __typename === 'Magazine'.

Predicates and Type-safe Usage

Test if a potato is the type that reads magazines, create a predicate function:

function isMagazineReader(potato: ChristmasCouchPotato): potato is MagazinePotato {
  return potato?.reading.__typename === 'Magazine'
}

Now we can use magazine potato-only fields safely:

const issueNum = isMagazineReader(potato) ? potato.reading.issueNumber : undefined

The magic, then, was when we created the MagazinePotato and could then discriminate it and conditionally use subtype fields.