TypeScript Type Guards — Complete Guide
In this tutorial, you will learn about TypeScript Type Guards. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript type guards are runtime checks that inform the compiler about a value's specific type within a code branch — they transform broad types like string | number into narrow, usable types through typeof, instanceof, in, and custom predicate functions.
What You'll Learn
typeoftype guards for primitivesinstanceoftype guards for classesintype guards for property existence- User-defined type predicates (
is) - Discriminated unions with
kindproperties
Why It Matters
Without type guards, every value of a union type is limited to the intersection of all members' properties. Type guards unlock the full interface of each member, enabling type-safe access to properties that exist only on specific variants.
Real-World Use
Durga Antivirus Pro's threat detection uses a discriminated union for scan results — a scan can be { status: "clean" }, { status: "infected"; threats: string[] }, or { status: "error"; message: string }. Type guards on the status property ensure only the relevant fields are accessed for each variant.
Learning Path
flowchart LR A[Index Signatures] --> B[Type Guards] B --> C[Narrowing] B --> D[You Are Here] C --> E[Recursive Types] E --> F[Variance]
typeof Type Guard
For primitive types, typeof is the simplest type guard:
function process(value: string | number | boolean): string {
if (typeof value === "string") {
return value.toUpperCase(); // value is string here
} else if (typeof value === "number") {
return value.toFixed(2); // value is number here
} else {
return value ? "true" : "false"; // value is boolean here
}
}
console.log(process("hello")); // HELLO
console.log(process(42.123)); // 42.12
console.log(process(true)); // true
Think of typeof as asking: "What primitive type is this value?" TypeScript narrows the type based on each possible result.
What typeof Can Check
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 123n // "bigint"
typeof function(){} // "function"
typeof {} // "object"
typeof null // "object" (bug — use value === null instead)
instanceof Type Guard
For class instances, instanceof checks the Prototype chain:
class APIError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
class NetworkError extends Error {
constructor(public url: string, message: string) {
super(message);
}
}
function handleError(error: APIError | NetworkError): string {
if (error instanceof APIError) {
return `API Error ${error.statusCode}: ${error.message}`;
} else {
return `Network Error on ${error.url}: ${error.message}`;
}
}
console.log(handleError(new APIError(404, "Not found")));
// API Error 404: Not found
console.log(handleError(new NetworkError("https://api.example.com", "Timeout")));
// Network Error on https://api.example.com: Timeout
in Type Guard
Check for property existence to narrow object types:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish): void {
if ("fly" in animal) {
animal.fly(); // Narrowed to Bird
} else {
animal.swim(); // Narrowed to Fish
}
}
User-Defined Type Predicates
Custom functions that return a type predicate (parameterName is Type):
interface Cat {
meow(): void;
purr(): void;
}
interface Dog {
bark(): void;
wagTail(): void;
}
function isCat(pet: Cat | Dog): pet is Cat {
return "meow" in pet;
}
function handlePet(pet: Cat | Dog): void {
if (isCat(pet)) {
pet.meow(); // Narrowed to Cat
pet.purr();
} else {
pet.bark(); // Narrowed to Dog
pet.wagTail();
}
}
Complex Predicate Example
interface User {
type: "user";
name: string;
email: string;
}
interface Admin {
type: "admin";
name: string;
permissions: string[];
}
function isAdmin(user: User | Admin): user is Admin {
return user.type === "admin";
}
function getPermissions(user: User | Admin): string[] {
if (isAdmin(user)) {
return user.permissions; // Narrowed to Admin
}
return ["read"]; // Default for regular users
}
Discriminated Unions
A discriminated union has a common property (the discriminant) with literal types:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
The never Exhaustiveness Check
Use never to ensure all union members are handled:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape); // Compile error if a case is missing
}
}
Null/Undefined Guards
function findUser(id: string): User | null | undefined {
// ...
}
function displayName(id: string): string {
const user = findUser(id);
if (user === null) {
return "User not found";
}
if (user === undefined) {
return "Not initialized";
}
return user.name; // Narrowed to User
}
Truthiness/Falsiness Guard
function getLength(value: string | null | undefined): number {
// Falsy check catches null, undefined, and empty string
if (!value) return 0;
return value.length; // Narrowed to string
}
Common Mistakes
1. Using typeof for Objects
if (typeof myVar === "object") {
// myVar could be: object, array, null, Date, etc.
// Not narrowed enough for practical use
}
Use in, instanceof, or custom predicates instead.
2. Forgetting That null Is "object" with typeof
if (typeof value === "object" && value !== null) {
// Safe: value is a non-null object
}
3. Not Providing Exhaustiveness Check
Without assertNever, adding a new union member won't cause compile errors — you'll get runtime bugs.
4. Overusing User-Defined Type Predicates
If a simple in or typeof check works, prefer it over a custom function. Predicates are best for complex logic.
5. Type Predicate Without Runtime Logic
function isString(x: unknown): x is string {
return true; // Always returns true — incorrect!
}
The predicate must match the actual runtime behavior.
Practice Questions
What is the difference between
typeofandinstanceof?typeofchecks primitive types (string, number, etc.).instanceofchecks class/constructor prototype chains.What is a user-defined type predicate? A function returning
x is Typethat tells TypeScript to narrow the type when the function returns true.What is a discriminated union? A union type where each member has a common literal property (the discriminant) that TypeScript uses for narrowing.
How does the
neverexhaustiveness check work? After handling all known union members, the remaining type should benever. If a new member is added, thenevercheck fails with a compile error.
Challenge: Create a discriminated union ApiResponse<T> with { status: "loading" }, { status: "success"; data: T }, and { status: "error"; message: string }. Write a function that handles each case and returns a React-ready string.
FAQ
Mini Project: Event Handler with Discriminated Unions
// src/event-handler.ts
type AppEvent =
| { type: "user_login"; userId: string; timestamp: Date }
| { type: "file_scan"; filePath: string; threats: number; duration: number }
| { type: "threat_detected"; threatName: string; severity: "low" | "medium" | "high" | "critical" }
| { type: "error"; code: number; message: string };
function handleEvent(event: AppEvent): void {
switch (event.type) {
case "user_login":
console.log(`User ${event.userId} logged in at ${event.timestamp}`);
break;
case "file_scan":
console.log(`Scanned ${event.filePath}: ${event.threats} threats in ${event.duration}ms`);
break;
case "threat_detected":
const icon = event.severity === "critical" ? "🚨" : "⚠️";
console.log(`${icon} ${event.severity.toUpperCase()}: ${event.threatName}`);
break;
case "error":
console.error(`Error ${event.code}: ${event.message}`);
break;
default:
const _exhaustive: never = event;
throw new Error(`Unknown event type: ${_exhaustive}`);
}
}
handleEvent({
type: "user_login",
userId: "usr-001",
timestamp: new Date(),
});
// User usr-001 logged in at ...
handleEvent({
type: "threat_detected",
threatName: "Trojan.Generic",
severity: "critical",
});
// CRITICAL: Trojan.Generic
What's Next
Now explore control flow analysis and narrowing in depth:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/24-index-signatures" >}} | Review index signatures |
| {{< ref "/programming-languages/typescript/26-narrowing" >}} | Control flow analysis and exhaustiveness |
| {{< ref "/programming-languages/typescript/27-recursive-types" >}} | Recursive and self-referential types |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro