Advanced Type Narrowing

Back

Loading concept...

🔍 Advanced Type Narrowing in TypeScript

The Story of the Shape-Shifting Detective

Imagine you’re a detective 🕵️ at a magical museum. Objects in this museum can look like anything at first—a painting, a statue, or a treasure chest. But you have special detective tools that help you figure out exactly what each object is.

TypeScript’s Type Narrowing is exactly like that! It helps you take something that could be many things and figure out exactly what it is.


🎭 What is Type Narrowing?

Think of a toy box with different toys inside. You reach in without looking. You might grab:

  • A teddy bear 🧸
  • A toy car 🚗
  • A puzzle piece 🧩

Type Narrowing is how you figure out which toy you grabbed—without guessing wrong!

type Toy = Bear | Car | Puzzle;

function playWith(toy: Toy) {
  // TypeScript: "Could be any toy!"
  // We need to narrow it down!
}

🏷️ Type Predicates: Your Personal Label Maker

A type predicate is like a label maker. You create a special function that says “YES, this IS a bear!” or “NO, this is NOT a bear!”

The Magic Words: is

function isBear(toy: Toy): toy is Bear {
  return (toy as Bear).fur !== undefined;
}

That toy is Bear part is the type predicate. It tells TypeScript: “If this function returns true, treat the toy as a Bear!”

See It In Action

function playWith(toy: Toy) {
  if (isBear(toy)) {
    // TypeScript knows: toy is Bear! 🧸
    console.log(toy.fur); // ✅ Safe!
  }
}

Real Example

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

// Our type predicate function
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // ✅ TypeScript knows!
  } else {
    pet.fly();  // ✅ Must be Bird!
  }
}

🔮 Inferred Type Predicates: TypeScript Reads Your Mind!

Sometimes TypeScript is SO smart, it figures out the type predicate by itself!

The Old Way (TypeScript < 5.5)

// You had to write this:
function isString(x: unknown): x is string {
  return typeof x === "string";
}

The New Way (TypeScript 5.5+)

// TypeScript now understands this!
const isString = (x: unknown) =>
  typeof x === "string";

// It automatically knows this narrows to string!

Array Filtering Magic ✨

This is where it gets REALLY cool:

const mixed = [1, "hello", 2, "world"];

// Before: got (string | number)[]
// After: TypeScript infers string[]!
const strings = mixed.filter(
  x => typeof x === "string"
);
// strings is now string[]! 🎉

How It Works

graph TD A["Function Returns Boolean"] --> B{Uses Type Checks?} B -->|Yes| C["TypeScript Infers Predicate"] B -->|No| D["Regular Boolean"] C --> E["Automatic Narrowing!"]

⚡ Assertion Functions: The Confidence Booster

An assertion function is like a strict teacher who says: “This MUST be true, or class is OVER!”

The asserts Keyword

function assertIsNumber(
  val: unknown
): asserts val is number {
  if (typeof val !== "number") {
    throw new Error("Not a number!");
  }
}

Why Use Them?

Regular predicates return true or false. Assertion functions throw errors if something’s wrong!

function process(value: unknown) {
  assertIsNumber(value);

  // If we reach here, TypeScript KNOWS
  // value is a number! 🎯
  console.log(value.toFixed(2)); // ✅ Safe!
}

asserts vs is

Feature is (Predicate) asserts
Returns boolean void
On false Returns false Throws error
Use case Conditional checks Validation

Assert Condition Pattern

function assert(
  condition: boolean,
  msg: string
): asserts condition {
  if (!condition) {
    throw new Error(msg);
  }
}

function divide(a: number, b: number) {
  assert(b !== 0, "Cannot divide by zero!");
  // TypeScript knows b is not 0 here
  return a / b;
}

🎪 Discriminated Unions: The Name Tag System

Imagine a party where everyone wears a name tag with their role: “Chef 👨‍🍳”, “Doctor 👩‍⚕️”, or “Artist 🎨”. You know exactly what each person does just by reading their tag!

The Discriminant Property

Every type has a unique tag (called a discriminant):

interface Circle {
  kind: "circle";  // 👈 The tag!
  radius: number;
}

interface Square {
  kind: "square";  // 👈 Different tag!
  size: number;
}

interface Triangle {
  kind: "triangle"; // 👈 Another tag!
  base: number;
  height: number;
}

type Shape = Circle | Square | Triangle;

Using the Tags

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TypeScript knows: shape is Circle!
      return Math.PI * shape.radius ** 2;

    case "square":
      // TypeScript knows: shape is Square!
      return shape.size ** 2;

    case "triangle":
      // TypeScript knows: shape is Triangle!
      return (shape.base * shape.height) / 2;
  }
}

Why This Works

graph TD A["Shape with kind"] --> B{Check kind} B -->|"circle"| C["Must be Circle"] B -->|"square"| D["Must be Square"] B -->|"triangle"| E["Must be Triangle"]

Common Patterns

API Responses:

type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; message: string }
  | { status: "loading" };

function handle(result: Result<User>) {
  if (result.status === "success") {
    console.log(result.data.name); // ✅
  }
}

🛡️ Exhaustiveness Checking: Never Miss a Case!

What if someone adds a new shape but forgets to handle it? Exhaustiveness checking catches this!

The never Trick

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // This line catches missing cases!
      const _exhaustive: never = shape;
      throw new Error(`Unknown: ${_exhaustive}`);
  }
}

What Happens If You Miss One?

// Someone adds a new shape:
interface Pentagon {
  kind: "pentagon";
  side: number;
}

type Shape = Circle | Square | Triangle | Pentagon;

// Now the switch shows an error! 🚨
// "Type 'Pentagon' is not assignable to 'never'"

Helper Function Pattern

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

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape); // Compile error if missing!
  }
}

Why never?

graph TD A["All Cases Handled"] --> B["Nothing Left"] B --> C["Only never Fits"] C --> D["If Something Remains..."] D --> E["Error! You Missed It!"]

🎯 Putting It All Together

Here’s our detective museum in action:

// Type predicate
function isArtwork(item: unknown): item is Artwork {
  return (item as Artwork).artist !== undefined;
}

// Assertion function
function assertValidItem(
  item: unknown
): asserts item is MuseumItem {
  if (!item || typeof item !== "object") {
    throw new Error("Invalid item!");
  }
}

// Discriminated union
type MuseumItem =
  | { type: "painting"; artist: string }
  | { type: "statue"; material: string }
  | { type: "artifact"; age: number };

// Exhaustiveness check
function describe(item: MuseumItem): string {
  switch (item.type) {
    case "painting":
      return `Painted by ${item.artist}`;
    case "statue":
      return `Made of ${item.material}`;
    case "artifact":
      return `${item.age} years old`;
    default:
      const _check: never = item;
      return _check;
  }
}

🌟 Quick Reference

Tool Purpose Keyword
Type Predicate Check & narrow is
Inferred Predicate Auto-detect (automatic)
Assertion Function Validate or throw asserts
Discriminated Union Tag-based narrowing Common property
Exhaustiveness Catch missing cases never

🚀 You Did It!

You’ve learned how TypeScript’s detective tools work:

  1. Type Predicates → Custom type checks with is
  2. Inferred Predicates → TypeScript figures it out!
  3. Assertion Functions → Validate or throw with asserts
  4. Discriminated Unions → Tag-based narrowing
  5. Exhaustiveness Checking → Never miss a case with never

Now go forth and narrow those types with confidence! 🎉

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.