Advanced TypeScript: Type Predicate Tips
Welcome to the Type Predicate Tips lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In an earlier lesson we saw type predicates, which are custom functions that work as type guards. We can use them to narrow union types like
A | Bto justAorB. Here'sisNumber, a type predicate function:>
function isNumber(maybeNumber: number | undefined): maybeNumber is number {return typeof maybeNumber === 'number';}We can use this function to narrow types from
number | undefinedto justnumber:- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
function numberOrUndefined(): number | undefined {return 1;}const n: number | undefined = numberOrUndefined();let n2: number;if (isNumber(n)) {n2 = n;} else {n2 = 0;}n2;Result:
1
There are two important things to know about type predicates in practice.
First, TypeScript blindly trusts us to implement type predicate functions correctly. This is different from most TypeScript code, where the compiler checks that every variable and argument matches exactly. If our type predicate function's body is wrong, then the types will also be wrong. They'll still be fully enforced, but the compiler will be enforcing the wrong types!
In the example above, we correctly identify number types with
typeof maybeNumber === 'number'. But what if we make a mistake? To find out, the next example is intentionally wrong in a way that the type system can't see.>
/* This function has a bug! TypeScript can't tell us when our type* predicate's body is wrong. */function isNumber(n: number | string): n is number {return typeof n === 'string';}const n: string = 'oh no';const n2: number = isNumber(n) ? n : 0;n2;Result:
'oh no'
We named our function
isNumber, so it should returntruewhen given a number. But we made a mistake: our function returnstruewhen given strings. TypeScript doesn't know our intention; it only knows that ifisNumberreturns true, it should treat the argumentnas anumber. The result is that TypeScript lets us put astringinside anumbervariable.Here's a code problem:
This
isStringfunction is a type predicate, but the function's body is incorrect. Fix the function so that it correctly identifies strings.function isString(s: string | undefined): s is string {return typeof s === 'string';}const s1: undefined = undefined;const s2: string = isString(s1) ? s1 : 'not a string';s2;- Goal:
'not a string'
- Yours:
'not a string'
Our advice is to heavily test your type predicates with an automated test suite. That will increase your confidence in the predicates, which will allow you to trust the type system as a whole.
The second thing to know is that type predicates are a great place to use
unknown. In the example above, we narrowed fromstring | undefinedtostring. But why should a function calledisStringonly takestring | undefined? What about other types that aren't strings, likebooleanorArray<{name: string}>?This is a perfect use case for
unknown: our function doesn't need to know what type its argumentshas. All it needs to do is checktypeof s. Let's rewriteisStringto take anunknownargument.>
function isString(s: unknown): s is string {return typeof s === 'string';}- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
isString(1);Result:
false
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
isString('Amir');Result:
true
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const s: unknown = 'a string';const s2: string = isString(s) ? s : 'not a string';s2;Result:
'a string'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const s: unknown = {name: 'Betty'};const s2: string = isString(s) ? s : 'not a string';s2;Result:
'not a string'
Why don't we use
anyhere instead ofunknown? It's becauseunknownstops us from making incorrect assumptions about the argument's value. For example, withfunction isString(s: any), TypeScript will let us access properties likes.thisPropertyDoesNotExist. Withfunction isString(s: unknown), TypeScript won't let us assume anything abouts, which is more safe.To recap, keep these two points in mind to prevent bugs when using type predicates:
- TypeScript trusts us to implement type predicates correctly. Test your predicate functions well to make sure that they do what you think they do.
- Type predicates can take unions as their arguments, or they can take
unknown. In most cases,unknownis the safest and most flexible option.