Skip to content
intermediate Phase 2 · TypeScript Advanced Types

Type Guards & Narrowing

Narrow types with typeof, instanceof, in, and custom type guards.

1h
0 problems
Topic Progress 0%

Built-in Type Guards

Built-in Type Guards

typeof

function processValue(value: string | number | boolean): string {
  if (typeof value === 'string') {
    return value.toUpperCase(); // TypeScript knows value is string
  }
  if (typeof value === 'number') {
    return value.toFixed(2); // TypeScript knows value is number
  }
  return value ? 'true' : 'false'; // TypeScript knows value is boolean
}

instanceof

class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
  }
}

function handleError(error: Error): string {
  if (error instanceof HttpError) {
    return `HTTP ${error.statusCode}: ${error.message}`;
  }
  if (error instanceof ValidationError) {
    return `Validation failed on ${error.field}: ${error.message}`;
  }
  return error.message;
}

in Operator

interface Circle { kind: 'circle'; radius: number }
interface Square { kind: 'square'; side: number }

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  if ('radius' in shape) {
    return Math.PI * shape.radius ** 2;
  }
  return shape.side ** 2;
}

Custom Type Guards

Custom Type Guards

Type Predicate Functions

interface Cat { type: 'cat'; meow(): void }
interface Dog { type: 'dog'; bark(): void }
type Pet = Cat | Dog;

function isCat(pet: Pet): pet is Cat {
  return pet.type === 'cat';
}

function isDog(pet: Pet): pet is Dog {
  return pet.type === 'dog';
}

function handlePet(pet: Pet): void {
  if (isCat(pet)) {
    pet.meow(); // TypeScript knows pet is Cat
  } else {
    pet.bark(); // TypeScript knows pet is Dog
  }
}

Real-World Example: API Response

interface SuccessResponse {
  ok: true;
  data: unknown;
}

interface ErrorResponse {
  ok: false;
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function isSuccess(response: ApiResponse): response is SuccessResponse {
  return response.ok === true;
}

function handleResponse(response: ApiResponse): void {
  if (isSuccess(response)) {
    console.log(response.data); // safe access
  } else {
    console.error(response.error); // safe access
  }
}

Checking for Property Existence

function processInput(input: unknown): string {
  if (
    typeof input === 'object' &&
    input !== null &&
    'name' in input &&
    typeof (input as any).name === 'string'
  ) {
    return (input as { name: string }).name;
  }
  return 'unknown';
}

Assertion Functions

Assertion Functions

assertNever

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}

type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape): number {
  switch (shape) {
    case 'circle': return Math.PI * 100;
    case 'square': return 100;
    case 'triangle': return 50;
    default: return assertNever(shape);
  }
}

Assertion Functions

function assertString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error('Value is null or undefined');
  }
}

// Usage
function processName(input: unknown): string {
  assertString(input); // narrows to string
  return input.toUpperCase(); // safe
}

function getLength(value: string | null): number {
  assertDefined(value); // narrows to string
  return value.length; // safe
}

Exhaustive Check Pattern

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

function handleMethod(method: HttpMethod): string {
  switch (method) {
    case 'GET': return 'Read';
    case 'POST': return 'Create';
    case 'PUT': return 'Update';
    case 'DELETE': return 'Remove';
    default: return assertNever(method);
  }
}

Control Flow Analysis

Control Flow Analysis

Smart Casting in Conditions

function process(data: string | number | null): string {
  // TypeScript narrows based on control flow
  if (data === null) return 'empty';

  // data is string | number here
  if (typeof data === 'string') {
    // data is string here
    return data.toUpperCase();
  }

  // data is number here
  return data.toString();
}

Truthiness Narrowing

function trimAndUpper(value: string | null | undefined): string {
  // null and undefined are falsy
  if (!value) return '';
  // value is string here
  return value.trim().toUpperCase();
}

Discriminated Unions as Guards

interface Loading { status: 'loading' }
interface Success { status: 'success'; data: string }
interface Error { status: 'error'; message: string }

type State = Loading | Success | Error;

function render(state: State): string {
  switch (state.status) {
    case 'loading': return 'Loading...';
    case 'success': return state.data;   // narrowed
    case 'error': return state.message;  // narrowed
  }
}

Best Practices

  • Prefer discriminated unions over type guards for closed unions
  • Use type predicates for runtime checks on external data
  • Use asserts for validation functions
  • Always handle the never case for exhaustive checks