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 | numberas both at once; with a guard, each branch gets a single precise type - Built-in options:
typeoffor primitives,instanceoffor class instances,infor property existence - User-defined guard: a function returning
param is SpecificType, where theiskeyword is not optional - Rule of thumb: working with union types or
unknown/any? Add a guard before accessing type-specific properties
Quick example
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
typeoforinstanceofbefore calling type-specific methods - API responses typed as
unknown- guard before accessing any property likeuser.name - Polymorphic components - use
"onClick" in propsto distinguishButtonPropsfromLinkProps - Skip it when working with a single concrete type, TypeScript already knows
- Prefer over
ascasting - 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
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.
// 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:
function isString(val: any): val is string {
return typeof val === "string";
}Mistake 3: instanceof on string primitives
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
let data: string | number = "test";
if (typeof data === "string") {
data = 42; // reassignment resets narrowing
}
data.toFixed(); // Compiler error - data is string | number againReassignment 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 propsto 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).successnarrows 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; useasonly 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
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
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
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 readyA concise answer to help you respond confidently on this topic during an interview.