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.