Advanced TypeScript: Type Predicates
Welcome to the Type Predicates lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Suppose that we have a
number | undefinedproperty, but we want to use it as anumber. If we try to do that directly, it's a type error:number | undefinedisn't assignable tonumber.>
type User = {name: string, age: number | undefined};const amir: User = {name: 'Amir', age: 36};const age: number = amir.age;age;Result:
type error: Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.
With conditional narrowing, we can narrow the age to just a
number. In the next example, we do that by usingtypeofas a type guard.>
type User = {name: string, age: number | undefined};const amir: User = {name: 'Amir', age: 36};let age: number;if (typeof amir.age === 'number') {age = amir.age;} else {age = 0;}age;Result:
36
typeofworks when we have a basic type likenumber. But what if we need a type guard for a complex type, like a nested object type with many properties? In this lesson, we'll see the answer: TypeScript lets us write "type predicates", which let us write our own functions that act as type guards.We'll write an
isAddressfunction as our example. Our first attempt is a regular function that returns aboolean.>
type Address = {postalCode: string, country: string};type User = {name: string, address: Address | undefined};function isAddress(address: Address | undefined): boolean {return address !== undefined;}isAddresstakes aAddress | undefined, so its body only needs to check foraddress !== undefined. If the argument isn't anundefined, we know it must be aAddress. But ifaddressallowed more types in its union, we'd need a more complex conditional.We can see that
isAddressonly returnstruewhen its argument is actually an address. However, nothing in our function tells the TypeScript compiler about that. If we try to doif (isAddress(address)), it doesn't act as a type guard. The variable's type is stillAddress | undefined, so trying to assign it to another variable of typeAddressis still a type error.>
type Address = {postalCode: string, country: string};type User = {name: string, address: Address | undefined};function isAddress(address: Address | undefined): boolean {return address !== undefined;}const amir: User = {name: 'Amir',address: {postalCode: '75010', country: 'France'}};let address: Address;if (isAddress(amir.address)) {address = amir.address;} else {address = {postalCode: 'unknown', country: 'unknown'};}address.postalCode;Result:
type error: Type 'Address | undefined' is not assignable to type 'Address'. Type 'undefined' is not assignable to type 'Address'.
A regular boolean function like
isAddressabove doesn't act as a type guard. However, with one small change it can.The next example is identical to the previous one, but with one small difference. We've changed
isAddress's return type frombooleantoaddress is Address. With that change,isAddressbecomes a type predicate. Now it works as a type guard!>
type Address = {postalCode: string, country: string};type User = {name: string, address: Address | undefined};function isAddress(address: Address | undefined): address is Address {return address !== undefined;}const amir: User = {name: 'Amir',address: {postalCode: '75010', country: 'France'}};let address: Address;/* Calling `isAddress` narrows the type of `amir.address` because it's a* type predicate. */if (isAddress(amir.address)) {address = amir.address;} else {address = {postalCode: 'unknown', country: 'unknown'};}address.postalCode;Result:
'75010'
The only new part here is the
address is Addressin place of a return value. That's the type predicate: it lets our function serve as a type guard.You can think of
address is Addressin the return type as answering the question: "isaddressaAddress?" IfisAddress(...)returnstrue, it tells the compiler thataddresshas the typeAddressfrom then on. If it returnsfalse, the types stay the same.Where does the term "type predicate" come from? In general, a predicate ("preh-dih-kit") function is any function that returns a
boolean. Type predicates are predicate functions that also change their arguments' static types.Returning to our example, the upgraded
isAddressstill works as a regular boolean function:>
type Address = {postalCode: string, country: string};function isAddress(address: Address | undefined): address is Address {return address !== undefined;}isAddress({postalCode: '75010', country: 'France'});Result:
true
Here's a code problem:
Add a type predicate to the
isAlbumfunction so it works as a type guard. (You won't need to change the function's body.)type Album = {name: string, copiesSold: number};type Artist = {name: string, topSellingAlbum: Album | undefined};function isAlbum(maybeAlbum: Album | undefined): maybeAlbum is Album {return maybeAlbum !== undefined;}const artist: Artist = {name: 'Pink Floyd',topSellingAlbum: {name: 'The Dark Side of the Moon',copiesSold: 24400000,},};let album: Album;if (isAlbum(artist.topSellingAlbum)) {album = artist.topSellingAlbum;} else {album = {name: 'unknown', copiesSold: 0};}album;- Goal:
{name: 'The Dark Side of the Moon', copiesSold: 24400000}- Yours:
{name: 'The Dark Side of the Moon', copiesSold: 24400000}
Type narrowing is a common challenge in TypeScript, and we solve it with type guards. We've now seen a few different kinds of type guards:
- Directly checking the type with the
typeofoperator, liketypeof aValue === 'number'. - Comparing against a value, like
aValue !== undefined. - Type predicate functions like the built-in
Array.isArray(), or theisAddressthat we wrote above.
- Directly checking the type with the
Some type predicates like
Array.isArraycome with the TypeScript compiler. However, they're implemented using the same type predicate syntax that we saw above. The only special thing about them is that they're defined in files that come with TypeScript itself.Two quick notes to wrap up this lesson. First, the term "type predicate" sometimes refers specifically to the
address is Addresssyntax. Other times, "type predicate" refers to an entire function using that return type syntax. We'll use it in both ways in this course.Second, let's recap the terminology, since there's a lot of it. We've seen type narrowing, type guards, and type predicates.
- Type narrowing lets us write separate code to handle union alternatives.
For example, if we have a
number | undefinedvariable, we can write separate code to handle thenumbercase vs. theundefinedcase. - Type guards are special expressions used inside of
ifconditions, likeif (Array.isArray(...))orif (typeof n === 'number'). We use them to narrow types. They also work insideswitchstatements and ternary expressions liketypeof x === 'number' ? x : y. - Type predicates let us write our own functions that act as type guards.
TypeScript comes with some type predicates predefined, like
Array.isArray, but we can define our own as well.
- Type narrowing lets us write separate code to handle union alternatives.
For example, if we have a