HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

30 Type guards and narrowing

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:

In this chapter, we explore type guards and narrowing.

30.1 Type guards and narrowing

30.1.1 Example: a type guard with ===

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:

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.

30.1.2 Example: a type guard with 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.

30.2 When do we need to narrow types?

These are examples of types being too general:

Note that these types are all union types!

30.2.1 The type unknown is usually too general

If 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.

30.3 Where can we use type guards?

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;
  }
}

30.3.1 Type guards can also affect parts of expressions

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.

30.4 What kinds of type guards are built-in?

In this section we explore TypeScript’s built-in type guards (expressions that evaluate to true or false).

30.4.1 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);
  }
}

30.4.2 Strict equality (===) 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.

30.4.3 Truthiness checks

30.4.3.1 Truthiness checks via 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:

30.4.3.2 Recommendation: avoid truthiness checks

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);
  }
}
30.4.3.3 Truthiness checks via logical And (&&) 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) {
    // ...
  }
}
30.4.3.4 Boolean() cannot be used as a type guard

The 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.

30.4.4 Checking for distinct properties via the operator 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.

30.4.4.1 The operator in also narrows to single properties

We 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).

30.4.5 Checking the value of a shared property (discriminated unions)

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();
  }
}

30.5 Narrowing via assignment

30.5.1 Narrowing storage locations with explicit types

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.

30.5.2 Narrowing variables without explicit types

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';

30.6 For which storage locations can we use type-narrowing?

30.6.1 Narrowing the types of dotted names

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:

30.6.2 Narrowing Array element types

30.6.2.1 The Array method .every() narrows

We 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>.

30.6.2.2 The Array method .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]
);

30.7 User-defined type guard functions

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.

Icon “details”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).

30.7.1 Example: 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);
  }
}

30.7.2 Example: helping .every() with a user-defined type guard

Let’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;
}

30.7.3 this-based type guards

In 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);
  }
}
30.7.3.1 Example: narrowing the type of a property

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.

30.7.4 Inferred type predicates

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);

30.7.5 Advanced example: user-defined type guard 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);
  }
}
30.7.5.1 Implementing isTypeOf() via conditional types

This 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.

30.7.5.2 Implementing isTypeOf() via a type lookup table

The 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.)

30.7.5.3 Implementing isTypeOf() via function overloading

Another 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.)