Sometimes a value nameOpt
has a type that is not specific enough – e.g., the type null | string
. Before we can do anything with nameOpt
, we have to check whether it is null
or a string:
null
or string
. That process is called narrowing.
In this chapter, we explore type guards and narrowing.
===
This is a first example:
function greet(nameOpt: null | string): string {
if (nameOpt === null) { // (A)
assertType<null>(nameOpt); // (B)
nameOpt = 'anonymous'; // (C)
assertType<string>(nameOpt);
} else {
assertType<string>(nameOpt);
}
assertType<string>(nameOpt); // (D)
return `Hello ${nameOpt}!`;
}
The initial type of nameOpt
, null | string
is too general:
if
statement in line A. Its condition is called a type guard: an expression with a boolean result that examines the type of nameOpt
.
In line B, we can see that the if
statement with the type guard did indeed narrow the type of nameOpt
inside the if-branch: At its start, it is null
. We can also see that in the else-branch, the type of nameOpt
is string
.
The assignment in line C also changes the type of nameOpt
. It doesn’t narrow its previous type, it narrows its original type.
Control flow analysis. After both branches of if
(in line D), the type of nameOpt
is string
because that’s what its type is at the end of both branches. So TypeScript traces what happens in both branches and adjusts the type accordingly. This kind of tracing is called control flow analysis.
The effect of a type guard is static and dynamic. It’s interesting that a type guard is always tied to a value that exists at runtime. Its has an effect at the type level and at the JavaScript level.
Narrowing produces subsets of types. In this chapter, we interpret types as sets of values, as explained in “What is a type in TypeScript? Two perspectives” (§13). Narrowing makes a type smaller: We go from a set of values to a proper subset.
typeof
In the following example, we use typeof
in a type guard:
function getScore(value: number | string): number {
if (typeof value === 'number') { // (A)
assertType<number>(value);
return value;
}
if (typeof value === 'string') { // (B)
assertType<string>(value);
return value.length;
}
throw new Error('Unexpected value: ' + value);
}
assert.equal(
getScore('*****'), 5
);
assert.equal(
getScore(3), 3
);
In this example, there are two type guards: one in line A and one in line B.
These are examples of types being too general:
Nullable types:
function func1(arg: null | string) {}
function func2(arg: undefined | string) {}
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function func3(attendee: Attendee) {}
Types of optional parameters:
function func4(arg?: string) {
assertType<string | undefined>(arg);
}
Note that these types are all union types!
unknown
is usually too generalIf a value has the type unknown
, we can do almost nothing with it and have to narrow its type first (line A):
function parseStringLiteral(stringLiteral: string): string {
// We use `unknown` instead of the less-safe `any`
const result: unknown = JSON.parse(stringLiteral);
if (typeof result === 'string') { // (A)
return result;
}
throw new Error('Not a string literal: ' + stringLiteral);
}
In other words: The type unknown
is too general and we must narrow it. In a way, unknown
is also a union type – the union of all types.
In if
statements:
function f(value: null | string): void {
if (value === null) {
assertType<null>(value);
} else {
assertType<string>(value);
}
}
In conditional expressions:
function f(value: null | string): void {
const result = value === null
? assertType<null>(value)
: assertType<string>(value)
;
}
In switch
statements:
function f(value: null | string): void {
switch (typeof value) {
case 'object':
assertType<null>(value);
break;
case 'string':
assertType<string>(value);
break;
}
}
If the left-hand side of the logical And operator (&&
) is a type guard then it affects its right-hand side (which may contain more uses of &&
):
function f(value: null | string): void {
if (value !== null && value.length > 0) {
// ...
}
}
The &&
operator stops evaluating if its left-hand side is falsy. Therefore, if we reach the right-hand side, we can be sure that value !== null
is true – which is why we can access value.length
.
With the logical Or operator (||
), we get a related effect:
function f(value: null | string): void {
if (value === null || value.length > 0) {
// ...
}
}
The ||
operator stops evaluating if its left-hand side is truthy. Therefore, if we reach the right-hand side, we can be sure that value === null
is false – which is why we can access value.length
.
In this section we explore TypeScript’s built-in type guards (expressions that evaluate to true
or false
).
typeof
, instanceof
, Array.isArray
These are three common built-in type guards:
function func(value: Function | Date | Array<number>): void {
if (typeof value === 'function') {
assertType<Function>(value);
}
if (value instanceof Date) {
assertType<Date>(value);
}
if (Array.isArray(value)) {
assertType<Array<number>>(value);
}
}
===
) and strict inequality(!==
)Strict equality can be used in a type guard:
function func(value: unknown): void {
if (value === 'abc') {
assertType<"abc">(value);
}
}
If an element of a union type is a singleton type (with a single value) then we can use ===
and !==
to narrow:
interface Book {
title: null | string;
isbn: string;
}
function getTitle(book: Book): string {
if (book.title !== null) {
assertType<string>(book.title);
return book.title;
}
if (book.title === null) {
assertType<null>(book.title);
return '(Untitled)';
}
throw new Error();
}
null
is a singleton type whose only member is the value null
.
if
Truthiness checks are related to equality checks. They also act as type guards:
function f(value: null | string): void {
if (value) {
assertType<string>(value);
} else {
assertType<null | string>(value); // (A)
}
}
At first glance, we may think that all strings go to the if-branch while all nulls go to the else-branch. Alas, that’s not the case:
value
is neither null
nor the empty string.
value
is either null
or the empty string. That explains the type in line A.
Truthiness is problematic, because it rejects non-nullish values such as empty strings, zero, etc. And TypeScript has no types to express “non-empty string”, “non-zero number”, etc.
That’s why I recommend to avoid it – code that uses it is more difficult to understand. Instead, we can use ===
and !==
for more explicit checks. With an explicit check, the previous example becomes less confusing:
function f(value: null | string): void {
if (value !== null) {
assertType<string>(value);
} else {
assertType<null>(value);
}
}
&&
) and logical Or (||
)The operators logical And (&&
) and logical Or (||
) can also perform truthiness checks:
function f1(value: null | string): void {
if (value && value.length > 0) {
// ...
}
}
function f2(value: null | string): void {
if (!value || value.length > 0) {
// ...
}
}
Boolean()
cannot be used as a type guardThe function Boolean()
cannot be used as a type guard:
function f(value: null | string): void {
if (Boolean(value)) {
assertType<null | string>(value); // (A)
}
}
Alas, there is no narrowing: In line A, the type of value
is not string
.
In principle, Boolean()
could be turned into a type guard function, but that would would have several downsides.
As an alternative to Boolean()
, we can use the prefix operator logical Negation (!
) twice. That does narrow:
function f(value: null | string): void {
if (!!value) {
assertType<string>(value);
}
}
However, both Boolean()
and !!
are truthiness checks and therefore best avoided.
in
The in
operator can be used in type guards – to check for distinct properties:
type FirstOrSecond =
| {first: string}
| {second: string}
;
function func(firstOrSecond: FirstOrSecond): void {
if ('first' in firstOrSecond) {
assertType<{ first: string }>(firstOrSecond);
}
}
Note that the following check would not have worked:
function func2(firstOrSecond: FirstOrSecond): void {
// @ts-expect-error: Property 'first' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.first !== undefined) {
// ···
}
}
The problem in this case is that, without narrowing, we can’t access property .first
of a value whose type is FirstOrSecond
.
in
also narrows to single propertiesWe can also use in
to narrow to a single property:
function func(obj: object): void {
if ('name' in obj) {
assertType<object & Record<'name', unknown>>(obj);
const value = obj.name;
assertType<unknown>(value);
}
}
This kind of narrowing only works if the left-hand side of the in
operator is a string literal (and not, e.g., a variable).
In a discriminated union, the components of a union type have (at least) one property in common whose value is different for each component. Such a property is called a discriminant.
Checking the value of a discriminant is a type guard:
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function getId(attendee: Attendee) {
switch (attendee.kind) {
case 'Teacher':
assertType<{ kind: 'Teacher', teacherId: string }>(attendee);
return attendee.teacherId;
case 'Student':
assertType<{ kind: 'Student', studentId: string }>(attendee);
return attendee.studentId;
default:
throw new Error();
}
}
In the previous example, .kind
is a discriminant: Each component of the union type Attendee
has this property, with a unique value.
An if
statement and equality checks work similarly to a switch
statement:
function getId(attendee: Attendee) {
if (attendee.kind === 'Teacher') {
assertType<{ kind: 'Teacher', teacherId: string }>(attendee);
return attendee.teacherId;
} else if (attendee.kind === 'Student') {
assertType<{ kind: 'Student', studentId: string }>(attendee);
return attendee.studentId;
} else {
throw new Error();
}
}
Assignment narrows the type of a storage location (such as a variable). If a variable x
has an explicit type, we can assign any value of that type to it – which narrows its type:
let x: null | string;
type _1 = Assert<Equal<
typeof x,
null | string
>>;
x = 'a';
type _2 = Assert<Equal<
typeof x,
string
>>;
x = null;
type _3 = Assert<Equal<
typeof x,
null
>>;
// @ts-expect-error: Type 'number' is not assignable to type 'string'.
x = 1;
After we assign a string, the type of x
is narrowed to string
. After we assign null
, the type of x
is narrowed to null
. At the end, we can see that we must stay within the confines of the type null | string
– otherwise, we get an error.
Note that, with type guards, we always narrow the current type, whereas with assignments, we narrow the original type.
If the compiler option noImplicitAny
is active (which it is if strict
is active) then each storage location must have a type – either an inferred type or an explicitly defined type. The only exception are variables defined via let
(var
works similarly):
let x; // implicitly has type `any`
type _1 = Assert<Equal<
typeof x, // (A)
undefined
>>;
x = 1;
type _2 = Assert<Equal<
typeof x,
number
>>;
x = 'a';
type _3 = Assert<Equal<
typeof x,
string
>>;
x = true;
type _4 = Assert<Equal<
typeof x,
boolean
>>;
x
initially has the implicit type any
. In line A, we observe its type, which is why its type is narrowed to undefined
. There are no restrictions with regard to what we can assign. Each time, the type is narrowed accordingly. There are no restrictions because we always narrow the original type of x
: the top type any
.
If we initialize a let
variable with a value then it does not have the implicit type any
:
let x = 1;
type _ = Assert<Equal<
typeof x,
number
>>;
// @ts-expect-error: Type 'string' is not assignable to type 'number'.
x = 'a';
We can narrow the types of properties (even of nested ones that we access via chains of property names):
type MyType = {
prop?: number | string,
};
function func(arg: MyType): void {
if (typeof arg.prop === 'string') {
assertType<string>(arg.prop); // (A)
[].forEach((x) => {
assertType<string | number | undefined>(arg.prop); // (B)
});
assertType<string>(arg.prop);
arg = {};
assertType<string | number | undefined>(arg.prop); // (C)
}
}
Let’s take a look at several locations in the previous code:
arg.prop
via a type guard.
.every()
narrowsWe can use method .every()
to narrow the type of Array elements:
function f(mixedValues: Array<undefined | null | number>): void {
if (mixedValues.every(x => x !== undefined && x !== null)) {
assertType<Array<number>>(mixedValues);
// @ts-expect-error: Argument of type 'null' is not assignable to
// parameter of type 'number'.
mixedValues.push(null); // (A)
}
}
Interestingly, TypeScript does not allow us to push the value null
to mixedValues
(line A) after we have narrowed its type to Array<number>
.
.filter()
produces Arrays with narrower types.filter()
produces Arrays that have narrower types (i.e., it doesn’t actually narrow existing types):
const mixedValues = [1, undefined, 2, null];
assertType<(number | null | undefined)[]>(mixedValues);
const numbers = mixedValues.filter(
x => x !== undefined && x !== null
);
assertType<number[]>(numbers);
assert.deepEqual(
numbers,
[1, 2]
);
TypeScript lets us define our own type guard functions – for example:
function isFunction(value: unknown): value is Function {
return typeof value === 'function';
}
The return type value is Function
is a type predicate. It is part of the type signature of isFunction()
:
assertType<(value: unknown) => value is Function>(isFunction);
A user-defined type guard function must always return booleans. If isFunction(x)
returns true
, TypeScript narrows the type of the actual argument x
to Function
:
function func(arg: unknown): void {
if (isFunction(arg)) {
assertType<Function>(arg); // type is narrowed
}
}
Note that TypeScript doesn’t care how we compute the result of a user-defined type guard function. That gives us a lot of freedom w.r.t. the checks we use. For example, we could have implemented isFunction()
as follows:
function isFunction(value: any): value is Function {
try {
value(); // (A)
return true;
} catch {
return false;
}
}
Note that we have to use the type any
for the parameter value
because the type unknown
does not let us make the function call in line A.
Terminology: “type guard function” vs. “type guard”
Custom type guard functions are often called custom type guards – even though they are only invoked in type guard expressions (and not type guards themselves).
isNonNullable()
The utility type NonNullable<T>
removes undefined
and null
from union types T
. In the next example, we use it to define a custom type guard:
function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== undefined && value !== null;
}
function f1(arg: null | string) {
if (isNonNullable(arg)) {
assertType<string>(arg);
}
}
function f2(arg: undefined | string) {
if (isNonNullable(arg)) {
assertType<string>(arg);
}
}
.every()
with a user-defined type guardLet’s look at an example where a type guard with .every()
doesn’t work properly. We’ll use the following utility type, which is explained in “A generic type for constructors: Class<T>
” (§23.3).
type Class<T> = abstract new (...args: Array<any>) => T;
After the type guard in line A, we’d expect the type of arr
to be Array<T>
. Alas, that’s not the case:
function f1<T>(arr: Array<unknown>, theClass: Class<T>): void {
if (arr.every(x => x instanceof theClass)) { // (A)
assertType<Array<unknown>>(arr)
}
}
We can fix this issue by turning the check in line A into the user-defined type guard isInstanceOf()
:
function f2<T>(arr: Array<unknown>, theClass: Class<T>): void {
if (arr.every(x => isInstanceOf(x, theClass))) {
assertType<Array<T>>(arr)
}
}
function isInstanceOf<T>(value: unknown, theClass: Class<T>): value is T {
return value instanceof theClass;
}
this
-based type guardsIn locations where we can use the type this
, we can implement type guards that use that type (line A and line B)
abstract class Shape {
isCircle(): this is Circle { // (A)
return this instanceof Circle;
}
isRectangle(): this is Rectangle { // (B)
return this instanceof Rectangle;
}
}
class Circle extends Shape {
center = new Point();
radius = 0;
}
class Rectangle extends Shape {
corner1 = new Point();
corner2 = new Point();
}
class Point {
x = 0;
y = 0;
}
The type guards .isCircle()
and .isRectangle()
enable us to go from the type of the abstract superclass Shape
to one of its subclasses – e.g.:
function f(shape: Shape): void {
if (shape.isCircle()) {
assertType<Circle>(shape);
}
}
In the following code, we use a this
-based type guard to narrow the type of a property:
class ValueContainer<T> {
value: null | T = null;
hasValue(): this is { value: T } & this {
return this.value !== null;
}
}
function f(stringContainer: ValueContainer<string>): void {
assertType<null | string>(stringContainer.value);
if (stringContainer.hasValue()) {
assertType<string>(stringContainer.value);
}
}
If the type guard .hasValue()
returns true
, we can be sure that .value
is not null
. Therefore, we narrow its type accordingly. The & this
is not really needed in this case but it ensures that the method still works if we add more properties: It means that we keep all of this
but intersect it with a type where .value
has a narrower type.
In some cases, TypeScript can infer a type predicate – e.g.:
const isNumber = (x: unknown) => typeof x === 'number';
assertType<
(x: unknown) => x is number
>(isNumber);
const isNonNullable = <T>(x: T) => x != null;
assertType<
<T>(x: T) => x is NonNullable<T>
>(isNonNullable);
This is why the Array methods .every()
and .filter()
can narrow Arrays with a callback that is not a type guard.
A type predicate can only be inferred if:
No type predicate is inferred for truthiness checks:
const isTruthy = (x: unknown) => !!x;
assertType<
(x: unknown) => boolean
>(isTruthy);
isTypeOf()
Let’s turn the JavaScript operator typeof
into the user-defined type guard isTypeOf()
– while fixing the following typeof
bug (null
should produce the result 'null'
):
> typeof null
'object'
> typeof {}
'object'
This is what using isTypeOf()
looks like:
//===== Using isTypeOf() at the JavaScript level =====
assert.equal(
isTypeOf('abc', 'string'), true
);
assert.equal(
isTypeOf(123, 'string'), false
);
// Fix `typeof` bug:
assert.equal(
isTypeOf(null, 'null'), true
);
assert.equal(
isTypeOf({}, 'null'), false
);
assert.equal(
isTypeOf({}, 'object'), true
);
assert.equal(
isTypeOf(null, 'object'), false
);
//===== Using isTypeOf() at the type level =====
function fn(value: unknown) {
if (isTypeOf(value, 'string')) {
assertType<string>(value);
}
// Fix `typeof` bug:
if (isTypeOf(value, 'object')) {
assertType<object>(value);
}
if (isTypeOf(value, 'null')) {
assertType<null>(value);
}
}
isTypeOf()
via conditional typesThis is a first attempt to implement typeof
in TypeScript:
function isTypeOf<
T extends string
>(
value: unknown, typeString: T
): value is TypeStringToType<T> {
switch (typeString) {
case 'null':
return value === null;
case 'object':
return typeof value === 'object' && value !== null;
default:
return typeof value === typeString;
}
}
type TypeStringToType<S extends string> =
S extends 'undefined' ? undefined :
S extends 'null' ? null :
S extends 'boolean' ? boolean :
S extends 'number' ? number :
S extends 'bigint' ? bigint :
S extends 'string' ? string :
S extends 'symbol' ? symbol :
S extends 'object' ? object :
S extends 'function' ? Function :
never
;
The generic type TypeStringToType
uses conditional types to translate from string literal types such as 'number'
to types such as number
.
isTypeOf()
via a type lookup tableThe following solution is similar to the previous one, but this time, we don’t use conditional types to translate from string literal types to types; we use an object literal type as a lookup table:
function isTypeOf<
T extends keyof TypeofLookupTable // (A)
>(
value: unknown, typeString: T
): value is TypeofLookupTable[T] {
switch (typeString) {
case 'null':
return value === null;
case 'object':
return typeof value === 'object' && value !== null;
default:
return typeof value === typeString;
}
}
type TypeofLookupTable = {
'undefined': undefined,
'null': null,
'boolean': boolean,
'number': number,
'bigint': bigint,
'string': string,
'symbol': symbol,
'object': object,
'function': Function,
};
The lookup table provides us with a nice benefit: We can restrict the type of typeString
to the keys of TypeofLookupTable
– which means that we get a compiler error if we make a typo:
// @ts-expect-error: Argument of type '"nmbr"' is not assignable to
// parameter of type 'keyof TypeofLookupTable'.
isTypeOf(123, 'nmbr')
(This approach is inspired by code by Ran Lottem.)
isTypeOf()
via function overloadingAnother option is to use function overloading:
function isTypeOf(value: unknown, typeString: 'undefined'): value is undefined;
function isTypeOf(value: unknown, typeString: 'null'): value is null;
function isTypeOf(value: unknown, typeString: 'boolean'): value is boolean;
function isTypeOf(value: unknown, typeString: 'number'): value is number;
function isTypeOf(value: unknown, typeString: 'bigint'): value is bigint;
function isTypeOf(value: unknown, typeString: 'string'): value is string;
function isTypeOf(value: unknown, typeString: 'symbol'): value is symbol;
function isTypeOf(value: unknown, typeString: 'object'): value is object;
function isTypeOf(value: unknown, typeString: 'function'): value is Function;
function isTypeOf(value: unknown, typeString: string): boolean {
switch (typeString) {
case 'null':
return value === null;
case 'object':
return typeof value === 'object' && value !== null;
default:
return typeof value === typeString;
}
}
(This approach is an idea by Nick Fisher.)