Skip to main content

What is typeguard in TypeScript

Type guard is a runtime check that tells the TypeScript compiler the exact type of a value inside a specific code block, narrowing a union type to a more precise subtype.

Theory

TL;DR

  • Think of type guards like a security checkpoint: past the check, TypeScript knows exactly what's inside
  • Without guards, TypeScript treats string | number as both at once; with a guard, each branch gets a single precise type
  • Built-in options: typeof for primitives, instanceof for class instances, in for property existence
  • User-defined guard: a function returning param is SpecificType, where the is keyword is not optional
  • Rule of thumb: working with union types or unknown/any? Add a guard before accessing type-specific properties

Quick example

typescript
function printLength(value: string | number) { if (typeof value === "string") { // TypeScript knows value is string here console.log(value.length); // works } else { // TypeScript knows value is number here console.log(value.toFixed(2)); // works } } printLength("hello"); // 5 printLength(3.14); // "3.14"

typeof value === "string" is the guard. TypeScript reads this check and updates its type knowledge for that branch only. No casting, no errors.

Key difference

Without a guard, TypeScript refuses to let you call .length on string | number because it cannot guarantee the type at runtime. A guard gives the compiler proof: inside this block, I checked. The narrowing lives only in that scope, once you exit the branch, the union type is back.

When to use

  • Union type params - add typeof or instanceof before calling type-specific methods
  • API responses typed as unknown - guard before accessing any property like user.name
  • Polymorphic components - use "onClick" in props to distinguish ButtonProps from LinkProps
  • Skip it when working with a single concrete type, TypeScript already knows
  • Prefer over as casting - casting skips the check; a guard proves the type at runtime

How the compiler handles this

TypeScript performs control flow analysis during type checking. It tracks possible types through every code path, recognizes built-in guards by their pattern, and intersects the original union with the asserted subtype inside that branch. User-defined guards work because of the is return type: TypeScript reads param is SpecificType as a contract and applies narrowing at every call site. No extra runtime cost beyond the check itself, V8 just executes the JavaScript normally.

Common mistakes

Mistake 1: using in but calling the wrong method

typescript
interface Cat { meow(): void; } interface Dog { bark(): void; } function pet(animal: Cat | Dog) { if ("meow" in animal) { animal.bark(); // Error: Cat doesn't have bark } }

in narrows by property existence. It confirms meow is there, not that bark is. Call what you actually checked for, or use a discriminated union with a literal type field instead.

Mistake 2: forgetting the is type predicate

This is the one that shows up most often in code reviews. The function looks correct, but the narrowing stops working silently.

typescript
// Returns boolean - no narrowing happens function isString(val: any): boolean { return typeof val === "string"; } if (isString(x)) { x.length; // TypeScript still sees any here }

TypeScript ignores plain boolean returns for narrowing purposes. The fix is one word:

typescript
function isString(val: any): val is string { return typeof val === "string"; }

Mistake 3: instanceof on string primitives

typescript
if (x instanceof String) { // Checks wrapper object, not primitive x.toUpperCase(); }

Primitives are not instances of anything. Use typeof x === "string" for strings, numbers, and booleans.

Mistake 4: mutating the variable inside the guard block

typescript
let data: string | number = "test"; if (typeof data === "string") { data = 42; // reassignment resets narrowing } data.toFixed(); // Compiler error - data is string | number again

Reassignment throws off TypeScript's control flow tracking. Use a const local variable inside the block if you need to transform the value.

Real-world usage

  • React - Mantine UI uses "onClick" in props to render a <button> or <a> from a single polymorphic component
  • Express - typeof req.body === "object" before accessing body properties in middleware
  • Zod - schema.safeParse(data).success narrows the result to the inferred type automatically
  • Next.js - req.method === "POST" narrows the method union before parsing the request body
  • Guard vs as - use guards for data from APIs or user input; use as only for trusted static shapes where you're certain

Follow-up questions

Q: What is a user-defined type guard?


A: A function that returns param is SpecificType. TypeScript uses the is predicate to narrow the type at every call site. Example: function isUser(obj: any): obj is User { return typeof obj.name === "string"; }.

Q: What is the difference between in, typeof, and instanceof?


A: typeof checks primitive types: string, number, boolean, function. instanceof checks class instances. in checks if a property exists on an object, which is useful for discriminating interfaces that share no common base class.

Q: How does TypeScript handle nested type guards?


A: Control flow analysis tracks types through nested if blocks, intersecting types progressively. The deeper the nesting, the narrower the type in each branch.

Q: Why does narrowing sometimes break inside loops?


A: TypeScript resets narrowing on each iteration because the variable might be reassigned between runs. Capture the narrowed value in a const inside the loop body to keep it stable.

Q: What is a discriminated union and how does it relate to type guards? (senior level)


A: A discriminated union uses a shared literal field like type: "success" | "error" as a discriminant. A switch on that field narrows the entire object shape automatically. It is more reliable than property checks and is the pattern Redux Toolkit uses for action types.

Examples

typeof: narrowing a primitive union

typescript
type Shape = string | number; function doubleIt(shape: Shape) { if (typeof shape === "number") { return shape * 2; // number } return shape.toUpperCase(); // string } console.log(doubleIt(5)); // 10 console.log(doubleIt("hello")); // "HELLO"

typeof is the simplest guard. After the early return in the number branch, TypeScript infers the remaining code can only be string.

in: polymorphic React component

typescript
interface ButtonProps { label: string; onClick: () => void; } interface LinkProps { href: string; children: string; } function InteractiveElement(props: ButtonProps | LinkProps) { if ("onClick" in props) { // narrowed to ButtonProps return <button onClick={props.onClick}>{props.label}</button>; } // narrowed to LinkProps return <a href={props.href}>{props.children}</a>; }

One function, two shapes, no casting. This pattern is common in component libraries like Mantine.

User-defined guard: validating API responses

typescript
function isUser(obj: unknown): obj is { name: string; id: number } { return ( typeof obj === "object" && obj !== null && typeof (obj as any).name === "string" && typeof (obj as any).id === "number" ); } function processItem(item: unknown) { if (isUser(item)) { console.log(item.name.toUpperCase()); // narrowed } else { console.log("Not a valid user"); } } processItem({ name: "Alice", id: 1 }); // "ALICE" processItem({}); // "Not a valid user"

This is the pattern Zod uses internally. The guard returns false for anything that does not match, so failures are always handled safely without try/catch.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?