AtomLearnAtomLearn
DashboardGoalsPathAchievementsReviewSign In
DashboardTypeScriptType Narrowing
TypeScriptNot Started

Type Narrowing

Narrow union types safely using typeof, instanceof, in, and discriminated unions

0%
Knowledge0%
Learn & Drill
Fluency0%
Drill & Speed
Retention0%
Mastery & Review
Confidence0%
All modes
Practice

Free tier: read the explanation here. Upgrade to Pro for Drills, Speed challenges & Mastery badges.

Upgrade
Knowledge
Fluency
Retention

Knowledge Debt detected

You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.

Explanation

Type narrowing is how TypeScript figures out the specific type of a value inside a conditional block. After a check, TypeScript *narrows* the union to the matching branch.

`typeof` narrowing — for primitives: ``typescript function format(val: string | number): string { if (typeof val === 'string') { return val.toUpperCase(); // TypeScript knows val is string here } return val.toFixed(2); // here val is number }

Truthiness narrowing — filters out null / undefined: ``typescript function greet(name: string | null): string { if (name) { return Hello, ${name}!; // name is string here (not null) } return 'Hello, stranger!'; }

`instanceof` narrowing — for class instances: ``typescript function logError(err: Error | string): void { if (err instanceof Error) { console.error(err.message); // err is Error } else { console.error(err); // err is string } }

`in` operator narrowing — checks if a property exists: ``typescript interface Cat { meow(): void; } interface Dog { bark(): void; } function makeSound(animal: Cat | Dog): void { if ('meow' in animal) { animal.meow(); // Cat } else { animal.bark(); // Dog } }

Discriminated unions — a shared kind/type field narrows the entire object: ``typescript interface Circle { kind: 'circle'; radius: number; } interface Rect { kind: 'rect'; width: number; height: number; } type Shape = Circle | Rect; function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'rect': return shape.width * shape.height; } } The kind` literal field is the *discriminant* — TypeScript uses it to determine which variant you have.

Type predicates — user-defined type guards: ``typescript function isString(val: unknown): val is string { return typeof val === 'string'; } if (isString(someVal)) { console.log(someVal.toUpperCase()); // TypeScript knows it's string }

Examples

typeof and in narrowing

`in` narrowing is ideal when two types share some but not all properties. The discriminated union pattern (below) is cleaner when you control the type design.

type StringOrArray = string | string[];

function normalize(input: StringOrArray): string[] {
  if (typeof input === 'string') {
    return [input]; // input narrowed to string
  }
  return input;     // input narrowed to string[]
}

console.log(normalize('hello'));         // ['hello']
console.log(normalize(['a', 'b', 'c'])); // ['a', 'b', 'c']

// in-based narrowing for object unions
interface Admin  { role: 'admin'; permissions: string[] }
interface Member { role: 'member'; planId: string }
type UserType = Admin | Member;

function describe(user: UserType): string {
  if ('permissions' in user) {
    return `Admin with ${user.permissions.length} permissions`; // Admin
  }
  return `Member on plan ${user.planId}`; // Member
}

Discriminated unions

The discriminated union pattern — a shared `type` or `kind` literal field — is the most ergonomic way to handle variant types in TypeScript. It works perfectly with `switch`.

// Each variant has a literal 'type' field — the discriminant
type Result<T> =
  | { type: 'success'; data: T }
  | { type: 'error'; message: string; code: number };

function handleResult<T>(result: Result<T>): void {
  switch (result.type) {
    case 'success':
      console.log('Data:', result.data);    // result is { type: 'success', data: T }
      break;
    case 'error':
      console.error(`Error ${result.code}: ${result.message}`);
      break;
  }
}

handleResult({ type: 'success', data: { id: 1, name: 'Alice' } });
handleResult({ type: 'error', message: 'Not found', code: 404 });

How well did you understand this?

Next in TypeScript

Utility Types

Continue