TypeScript Type Guards
last modified October 15, 2026
A variable of a union type, such as string | number,
can hold values of several types. Before using it in a way that only one of
them supports, the program has to check which type it actually has. A type
guard is such a runtime check that TypeScript understands: inside the branch
where the check succeeds, the compiler narrows the variable to the more
specific type. This process is called narrowing.
TypeScript recognizes the typeof,
instanceof, and in operators, comparisons with
===, truthiness checks, and user-defined type guard functions.
It follows the control flow of the program, so a check also affects the code
after an early return.
Using typeof
The typeof operator returns a string with the runtime
type of a primitive value. Comparing it with a literal narrows the
variable.
function printValue(value: string | number): void {
if (typeof value === "string") {
console.log(`String: ${value.toUpperCase()}`);
} else {
console.log(`Number: ${value.toFixed(2)}`);
}
}
printValue("hello");
printValue(42);
Inside the if block, value has the
string type, so toUpperCase is available. In the
else block, the only remaining possibility is
number, and TypeScript allows toFixed. Calling
toFixed before the check would be a compile-time error, because
strings do not have this method.
The typeof operator can return
"string", "number", "bigint",
"boolean", "symbol", "undefined",
"object", and "function". Note that
typeof null is "object", so a
typeof value === "object" check alone does not exclude
null.
$ npx tsx typeof_guard.ts String: HELLO Number: 42.00
Using instanceof
The instanceof operator checks whether an object was
created by a class or one of its subclasses. It narrows the variable to the
class type.
class Dog {
bark(): string {
return "Woof!";
}
}
class Cat {
meow(): string {
return "Meow!";
}
}
function speak(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return animal.bark();
}
return animal.meow();
}
console.log(speak(new Dog()));
console.log(speak(new Cat()));
After the instanceof Dog check returns from the
function, TypeScript knows that the rest of the function can only be reached
with a Cat, so no else is needed. The
instanceof operator works only with classes, which exist at
runtime. Interfaces and type aliases disappear after compilation and cannot be
checked this way.
$ npx tsx instanceof_guard.ts Woof! Meow!
Using the in Operator
The in operator checks whether an object has a
property. TypeScript uses it to narrow unions of object types that differ in
their properties.
interface Car {
drive(): string;
}
interface Boat {
sail(): string;
}
function move(vehicle: Car | Boat): string {
if ("drive" in vehicle) {
return vehicle.drive();
}
return vehicle.sail();
}
console.log(move({ drive: () => "Driving on the road" }));
console.log(move({ sail: () => "Sailing on the sea" }));
Only Car has a drive property, so after
the check vehicle is a Car. Unlike
instanceof, the in operator works with plain
objects and interfaces.
$ npx tsx in_operator.ts Driving on the road Sailing on the sea
Equality Narrowing with Literal Types
Comparing a value with a literal using === or
!== narrows union types of literals. The same applies to the
cases of a switch statement.
type Status = "success" | "error" | "loading";
function describe(status: Status): string {
if (status === "loading") {
return "Please wait...";
}
switch (status) {
case "success":
return "Operation succeeded";
case "error":
return "Operation failed";
}
}
console.log(describe("success"));
console.log(describe("error"));
console.log(describe("loading"));
After the if statement, the type of
status is narrowed to "success" | "error". The
switch handles both remaining values, so TypeScript knows that
every path returns a string and does not report a missing
return statement. If a new status were added to the type, the
compiler would report that the function lacks a return value for it.
$ npx tsx literal_guard.ts Operation succeeded Operation failed Please wait...
Null Checks
With strictNullChecks, values that may be
null or undefined must be checked before use. A
comparison with null is a type guard.
function printLength(value: string | null | undefined): void {
if (value == null) {
console.log("No value");
return;
}
console.log(`Length: ${value.length}`);
}
printLength(null);
printLength(undefined);
printLength("Hello");
The loose comparison value == null is true for both
null and undefined. After the early
return, value has the string type.
With the strict comparison value === null, the type would still
include undefined.
$ npx tsx null_check.ts No value No value Length: 5
Truthiness Narrowing
A value used as a condition is narrowed by its truthiness. This is
short, but it also excludes valid falsy values such as 0 and the
empty string.
function greet(name?: string): string {
if (name) {
return `Hello, ${name}!`;
}
return "Hello, stranger!";
}
function formatCount(count: number | undefined): string {
if (count) {
return `${count} items`;
}
return "count unknown";
}
console.log(greet("Alice"));
console.log(greet(""));
console.log(formatCount(3));
console.log(formatCount(0));
For greet, treating the empty string like a missing
name is probably fine. For formatCount, it is a bug: a count of
0 is reported as unknown, because 0 is falsy. A check such as
count !== undefined narrows the type just as well and keeps
zero.
$ npx tsx truthiness.ts Hello, Alice! Hello, stranger! 3 items count unknown
Custom Type Predicates
A function whose return type has the form
parameter is Type is a user-defined type guard. When it returns
true, TypeScript narrows the argument to the given type.
interface Bird {
name: string;
fly(): string;
}
interface Fish {
name: string;
swim(): string;
}
function isBird(animal: Bird | Fish): animal is Bird {
return "fly" in animal;
}
function move(animal: Bird | Fish): string {
if (isBird(animal)) {
return `${animal.name}: ${animal.fly()}`;
}
return `${animal.name}: ${animal.swim()}`;
}
const eagle: Bird = { name: "eagle", fly: () => "flying high" };
const salmon: Fish = { name: "salmon", swim: () => "swimming upstream" };
console.log(move(eagle));
console.log(move(salmon));
Moving a check into a function makes it reusable and gives it a
name. The compiler trusts the predicate: it does not verify that the body of
isBird really checks for a Bird. A wrong predicate
leads to the same runtime errors as a wrong type assertion, so it should be
kept simple.
$ npx tsx custom_predicate.ts eagle: flying high salmon: swimming upstream
Type Guards with Arrays
Type guards are often used in callbacks of array methods. A type
predicate passed to filter also changes the type of the
resulting array.
function isString(value: unknown): value is string {
return typeof value === "string";
}
const items: (string | number)[] = ["apple", 42, "banana", 100];
for (const item of items) {
if (typeof item === "string") {
console.log(`String: ${item}`);
} else {
console.log(`Number: ${item}`);
}
}
const words = items.filter(isString);
console.log(words.map(word => word.toUpperCase()));
const total = items
.filter((item): item is number => typeof item === "number")
.reduce((sum, n) => sum + n, 0);
console.log(total);
The loop narrows each element separately. The
filter(isString) call returns a string[], not a
(string | number)[], so string methods can be used on the
result. The second filter uses an inline predicate with an
explicit return type. Since TypeScript 5.5, the compiler can also infer such a
predicate from a simple callback like
item => typeof item === "number".
$ npx tsx array_guard.ts String: apple Number: 42 String: banana Number: 100 [ 'APPLE', 'BANANA' ] 142
Discriminated Unions
A discriminated union is a union of object types that share a
property with a different literal type in each member, often called
kind or type. Checking this property narrows the
whole object.
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };
type Shape = Square | Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size ** 2;
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
const shapes: Shape[] = [
{ kind: "square", size: 5 },
{ kind: "circle", radius: 3 },
{ kind: "rectangle", width: 4, height: 2 },
];
for (const shape of shapes) {
console.log(`${shape.kind}: ${area(shape).toFixed(2)}`);
}
In each case, shape has exactly one of
the three types, so only its own properties are available. This pattern is
more robust than checking for properties with in: the
kind values are explicit, and the compiler reports a missing
case when a new shape is added. The never
type tutorial shows how to make such checks exhaustive.
$ npx tsx object_guard.ts square: 25.00 circle: 28.27 rectangle: 8.00
Type Guards with Error Classes
In a catch block, the error has the
unknown type in strict mode. The instanceof
operator distinguishes between error classes, including custom ones.
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = "ValidationError";
}
}
function parseAge(text: string): number {
const age = Number(text);
if (Number.isNaN(age)) {
throw new ValidationError("age", `'${text}' is not a number`);
}
if (age < 0) {
throw new RangeError("age cannot be negative");
}
return age;
}
for (const input of ["42", "abc", "-5"]) {
try {
console.log(`age: ${parseAge(input)}`);
} catch (e) {
if (e instanceof ValidationError) {
console.log(`invalid field ${e.field}: ${e.message}`);
} else if (e instanceof Error) {
console.log(`${e.name}: ${e.message}`);
} else {
console.log("unknown error", e);
}
}
}
The more specific class is checked first, because a
ValidationError is also an Error. After the first
check, the custom field property is available. The last branch
handles values that are not errors at all, since JavaScript allows throwing
any value.
$ npx tsx class_guard.ts age: 42 invalid field age: 'abc' is not a number RangeError: age cannot be negative
Combining Type Guards
Several guards can be combined in one chain. Each check removes types from the union, so later branches work with what is left.
type Input = string | number | boolean | string[] | null;
function describe(value: Input): string {
if (value === null) {
return "null";
}
if (Array.isArray(value)) {
return `array of ${value.length} strings`;
}
if (typeof value === "string") {
return `string "${value}"`;
}
if (typeof value === "number") {
return `number ${value}`;
}
return `boolean ${value}`;
}
const inputs: Input[] = ["hello", 42, true, ["a", "b"], null];
for (const input of inputs) {
console.log(describe(input));
}
The null check comes first; the
Array.isArray function narrows to the array type; the two
typeof checks handle strings and numbers. In the final line,
value can only be a boolean; TypeScript knows this
without any further check.
$ npx tsx combined_guards.ts string "hello" number 42 boolean true array of 2 strings null
Assertion Functions
An assertion function throws an error if a condition does not hold.
Its return type asserts value is Type tells the compiler that
the value has the given type after the function returns normally.
function assertIsString(value: unknown, name: string): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`${name} must be a string, got ${typeof value}`);
}
}
function shout(input: unknown): string {
assertIsString(input, "input");
return input.toUpperCase();
}
console.log(shout("hello"));
try {
console.log(shout(123));
} catch (e) {
console.log(e instanceof Error ? e.message : e);
}
After the call to assertIsString, the
input parameter has the string type in the rest of
the function, without an if block. Assertion functions are
useful for validating arguments at the start of a function. When an
assertion function is written as an arrow function stored in a variable, the
variable needs an explicit type annotation; otherwise TypeScript reports an
error at the call.
$ npx tsx assert_function.ts HELLO input must be a string, got number
Best Practices
- Prefer Built-in Checks:
typeof,instanceof,in, and===are reliable because the compiler understands them directly. - Use Discriminated Unions: Give related object types a common
kindproperty instead of telling them apart by optional properties. - Mind Falsy Values: Truthiness checks also exclude
0,"", andfalse; compare withundefinedornullwhen these values are valid. - Keep Predicates Honest: The compiler trusts custom type predicates, so their body must really check what the signature promises.
- Return Early: Handle special cases with an early
returnor an assertion function; the narrowed type then applies to the rest of the function. - Check Errors in catch: Narrow the caught value with
instanceofbefore using its properties.
Source
TypeScript Handbook: Narrowing
This tutorial covered TypeScript type guards. They connect runtime checks with compile-time types, so that a value of a union type can be used safely in each branch.
Author
List all TypeScript tutorials.