Skip to content

Type Guards

Type guards are TypeScript constructs that narrow down the type of a variable within a conditional block, enabling type-safe code.

Had to use type assertions:

function process(value: string | number) {
// TypeScript doesn't know which type
const len = (value as string).length; // Unsafe!
}

Type guards provide safe type narrowing:

// typeof type guard
function format(value: string | number): string {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
} else {
// TypeScript knows value is number here
return value.toFixed(2);
}
}
// instanceof type guard
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// in operator type guard
interface Car {
drive(): void;
}
interface Boat {
sail(): void;
}
function move(vehicle: Car | Boat) {
if ("drive" in vehicle) {
vehicle.drive();
} else {
vehicle.sail();
}
}
// Custom type guard (type predicate)
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string
console.log(value.toUpperCase());
}
}
// Array type guard
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) &&
value.every(item => typeof item === "string");
}
// Discriminated unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: 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;
}
}
  • Type safety: Compile-time guarantees
  • Autocomplete: IDE knows exact type
  • No assertions: Avoid unsafe type assertions
  • Readable: Clear type checking logic
  • Maintainable: Compiler catches errors
  • Custom guards must return value is Type
  • Guards only work within their scope
  • Can combine multiple guards
  • Discriminated unions use literal types
  • Nullable type guards
// Truthiness narrowing
function print(value: string | null | undefined) {
if (value) {
// value is string here
console.log(value.toUpperCase());
}
}
// Equality narrowing
function compare(x: string | number, y: string | boolean) {
if (x === y) {
// Both must be string here
x.toUpperCase();
y.toUpperCase();
}
}
// Never type with exhaustive checking
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Action =
| { type: "increment" }
| { type: "decrement" };
function reducer(action: Action) {
switch (action.type) {
case "increment":
return;
case "decrement":
return;
default:
assertNever(action); // Ensures all cases covered
}
}
  1. Create a type guard for checking if value is a number array

    Answer
    function isNumberArray(value: unknown): value is number[] {
    return Array.isArray(value) &&
    value.every(item => typeof item === "number");
    }
  2. Narrow string | null to just string

    Answer
    function process(value: string | null) {
    if (value !== null) {
    // value is string here
    console.log(value.toUpperCase());
    }
    }
  3. What’s a discriminated union?

    Answer A union type where each variant has a common property (discriminant) with a unique literal type, enabling type narrowing via that property.