Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. TypeScript
  4. TypeScript Type Narrowing Cheat Sheet
TypeScriptCheat Sheet

TypeScript Type Narrowing Cheat Sheet

Every narrowing technique in one reference -- typeof, instanceof, in, type predicates, discriminated unions, exhaustive checking, and assertion functions.

On this page
  1. 1typeof Narrowing
  2. 2Truthiness Narrowing
  3. 3in Operator
  4. 4instanceof
  5. 5Type Guard Functions (x is T)
  6. 6Discriminated Unions
  7. 7Exhaustive Checking with never
  8. 8Assertion Functions (asserts x is T)
  9. 9Narrowing Gotchas
typeof NarrowingTruthiness Narrowingin OperatorinstanceofType Guard Functions (x is T)Discriminated UnionsExhaustive Checking with neverAssertion Functions (asserts x is T)Narrowing Gotchas

typeof Narrowing

TypeScript narrows the type after a `typeof` check to the matching primitive.

Narrow string | number
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase();  // => value is string
  }
  return value.toFixed(2);  // => value is number
}

format('hello');  // => 'HELLO'
format(3.14159);  // => '3.14'
typeof return values
// typeof returns one of these strings:
// 'string' | 'number' | 'bigint' | 'boolean'
// | 'symbol' | 'undefined' | 'object' | 'function'

function handle(x: unknown) {
  if (typeof x === 'function') {
    x();  // => x is Function
  }
  if (typeof x === 'object') {
    // x is object | null  (typeof null === 'object')
  }
}

`typeof null` returns `"object"` -- always check for null separately when narrowing objects.

Truthiness Narrowing

Truthy/falsy checks narrow out `null`, `undefined`, `0`, `""`, and `false`.

Narrow out null and undefined
function greet(name: string | null | undefined): string {
  if (name) {
    return `Hello, ${name}`;  // => name is string
  }
  return 'Hello, stranger';
}
Truthiness pitfall with empty string and 0
function display(value: string | number | null) {
  if (value) {
    console.log(value);  // => string | number
  }
  // But "" and 0 are falsy -- they fall through to here
  // This might not be what you want
}

// Safer: check for null/undefined explicitly
function displaySafe(value: string | number | null) {
  if (value !== null) {
    console.log(value);  // => string | number (includes "" and 0)
  }
}

Truthiness narrowing eliminates all falsy values, not just null/undefined. Use explicit null checks when 0 or "" are valid inputs.

in Operator

The `in` operator narrows based on whether a property exists on the value.

Narrow a union by property presence
interface Dog { bark(): void; breed: string }
interface Cat { meow(): void; indoor: boolean }

function handlePet(pet: Dog | Cat) {
  if ('bark' in pet) {
    pet.bark();     // => pet is Dog
    pet.breed;      // => type: string
  } else {
    pet.meow();     // => pet is Cat
    pet.indoor;     // => type: boolean
  }
}
Narrow unknown objects
function processResponse(data: unknown) {
  if (
    typeof data === 'object' &&
    data !== null &&
    'error' in data
  ) {
    // data is narrowed to: object & Record<'error', unknown>
    console.log((data as { error: string }).error);
  }
}

The `in` operator requires the left side to be a string literal and the right side to be an object type.

instanceof

Narrows to a class instance by checking the prototype chain.

Narrow to a class type
class ApiError {
  constructor(public status: number, public message: string) {}
}

class NetworkError {
  constructor(public url: string) {}
}

function handleError(err: ApiError | NetworkError) {
  if (err instanceof ApiError) {
    console.log(err.status);   // => err is ApiError
  } else {
    console.log(err.url);      // => err is NetworkError
  }
}
instanceof with built-in types
function process(value: Date | string) {
  if (value instanceof Date) {
    value.getFullYear();   // => value is Date
  } else {
    value.toUpperCase();   // => value is string
  }
}

instanceof only works with classes and constructors, not interfaces or type aliases (those are erased at runtime).

Type Guard Functions (x is T)

A function with return type `x is T` tells TypeScript to narrow the parameter when the function returns true.

Basic type predicate
interface User {
  id: number;
  name: string;
  email: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    'email' in value
  );
}

function process(data: unknown) {
  if (isUser(data)) {
    console.log(data.name);   // => data is User
    console.log(data.email);  // => type: string
  }
}
Filter arrays with type guards
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function isCircle(shape: Shape): shape is Extract<Shape, { kind: 'circle' }> {
  return shape.kind === 'circle';
}

const shapes: Shape[] = [
  { kind: 'circle', radius: 5 },
  { kind: 'square', side: 3 },
  { kind: 'circle', radius: 10 },
];

const circles = shapes.filter(isCircle);
// circles => type: { kind: 'circle'; radius: number }[]

Array.filter with a type guard narrows the array element type. Without the predicate, filter returns Shape[].

Discriminated Unions

A shared literal property (the discriminant) lets TypeScript narrow union members automatically.

Discriminated union with switch
type ApiResult<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }
  | { status: 'loading' };

function render(result: ApiResult<string[]>) {
  switch (result.status) {
    case 'success':
      console.log(result.data.join(', '));  // => data is string[]
      break;
    case 'error':
      console.log(result.error);            // => error is string
      break;
    case 'loading':
      console.log('Loading...');
      break;
  }
}
Nested discriminated unions
type Action =
  | { type: 'fetch'; url: string }
  | { type: 'cache'; key: string; ttl: number }
  | { type: 'transform'; fn: (data: string) => string };

function execute(action: Action) {
  if (action.type === 'cache') {
    console.log(action.key, action.ttl);  // => both accessible
  }
}

Exhaustive Checking with never

Assign to `never` in the default branch to get a compile error when you forget a case.

Exhaustive switch
type Status = 'pending' | 'active' | 'archived';

function getLabel(status: Status): string {
  switch (status) {
    case 'pending':  return 'Waiting';
    case 'active':   return 'In Progress';
    case 'archived': return 'Done';
    default: {
      const _exhaustive: never = status;
      return _exhaustive;  // => Error if a case is missed
    }
  }
}
Helper function for exhaustive checks
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

type Shape = { kind: 'circle'; r: number } | { kind: 'square'; s: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.r ** 2;
    case 'square': return shape.s ** 2;
    default: return assertNever(shape);
  }
}

If you add a new union member later, the default branch errors at compile time -- forcing you to handle the new case.

Assertion Functions (asserts x is T)

Assertion functions narrow the type for the rest of the scope if they return without throwing.

Assert non-null
function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`${name} must be defined`);
  }
}

function process(config: { apiKey?: string }) {
  assertDefined(config.apiKey, 'apiKey');
  // After assertion, apiKey is string (not string | undefined)
  console.log(config.apiKey.toUpperCase());
}
Assert a specific type
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

function handle(input: unknown) {
  assertIsString(input);
  // input is now string for the rest of this scope
  console.log(input.toUpperCase());
}

Assertion functions narrow the type after the call site -- unlike type guards which narrow inside a conditional block.

Narrowing Gotchas

Situations where narrowing doesn't work the way you'd expect.

Closures reset narrowing
function example(value: string | null) {
  if (value !== null) {
    // value is string here

    setTimeout(() => {
      // value is string | null again -- TypeScript can't guarantee
      // it wasn't reassigned between now and when the callback runs
      // value.toUpperCase();  // => Error
    }, 100);
  }
}

// Fix: capture in a const
function exampleFixed(value: string | null) {
  if (value !== null) {
    const captured = value;  // => type: string (const, won't change)
    setTimeout(() => {
      captured.toUpperCase();  // => OK
    }, 100);
  }
}
Reassignment resets narrowing
function process(value: string | number) {
  if (typeof value === 'string') {
    value.toUpperCase();  // => OK, value is string

    value = 42;           // Reassignment

    // value.toUpperCase();  // => Error: value is now number
    value.toFixed(2);     // => OK, value is number
  }
}

TypeScript tracks assignments. Any reassignment resets the narrowed type to whatever the new value's type is.

Property narrowing doesn't survive method calls
interface Config {
  debug?: boolean;
  verbose?: boolean;
}

function setup(config: Config) {
  if (config.debug) {
    console.log('Debug on');  // => config.debug is true

    doSomething(config);
    // After a function call, config.debug might have changed
    // config.debug is boolean | undefined again

    // Fix: destructure first
    const { debug } = config;
    if (debug) {
      doSomething(config);
      console.log(debug);  // => still true (const binding)
    }
  }
}

function doSomething(config: Config) { /* ... */ }
Learn TypeScript in Depth
Type Narrowing →
See Also
Union & Intersection Types →Interface vs Type →

Start Practicing TypeScript

Free daily exercises with spaced repetition. No credit card required.

← Back to TypeScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.