06open 25 min

Advanced topic: narrowing and discriminated unions for modeling dynamic Python-style data safely

Use narrowing and discriminated unions to model flexible real-world data without falling back to untyped Python-style assumptions.

by the end of this lesson you can

  • Defines a union with a clear discriminant field
  • Narrows before reading branch-specific properties
  • Avoids as assertions where proper narrowing would be clearer

Overview

This is the advanced lesson because it captures where many Python developers either start loving TypeScript or start fighting it. Dynamic payloads, event shapes, and multi-state APIs are common in both ecosystems, but TypeScript gives you tools to describe that flexibility precisely instead of leaving it informal.

In Python, you often

handle flexible dictionaries or object variants through conventions, key checks, and runtime branching that readers must infer.

In TypeScript, the common pattern is

to encode legal variants in unions and let narrowing guide each branch so the compiler and reader both know what is valid.

why this difference matters

Advanced TypeScript is not about cleverness. It is about making dynamic data explicit enough that refactors stay safe and callers stop guessing which fields are available in each branch.

Python

payload = {"kind": "user", "name": "Ana"}
if payload["kind"] == "user":
    print(payload["name"])

TypeScript

type Payload =
  | { kind: "user"; name: string }
  | { kind: "system"; code: number };

function handlePayload(payload: Payload) {
  if (payload.kind === "user") {
    console.log(payload.name);
  }
}

Deeper comparison

Python version

response = {"state": "error", "message": "bad token"}
if response["state"] == "error":
    print(response["message"])

TypeScript version

type Response =
  | { state: "ok"; data: User }
  | { state: "error"; message: string };

function render(response: Response) {
  switch (response.state) {
    case "ok":
      return response.data.name;
    case "error":
      return response.message;
  }
}

Reflect

Why is narrowing often the missing skill that turns TypeScript from annoying compiler into a useful modeling tool for dynamic application data?

what a strong answer notices

A strong answer mentions that narrowing converts broad possibilities into safe concrete branches, making variant-heavy data easier to use than loosely typed dictionaries and assertions.

Rewrite

Rewrite this flexible Python-style event handling into TypeScript so each legal event shape is modeled and narrowed safely.

Rewrite this Python

event = {"type": "created", "id": 1}
if event["type"] == "created":
    print(event["id"])

what good looks like

  • Defines a union with a clear discriminant field
  • Narrows before reading branch-specific properties
  • Avoids as assertions where proper narrowing would be clearer

Practice

Design TypeScript types and handling code for an event payload system with three variants, and explain how narrowing protects future refactors.

success criteria

  • Uses discriminated unions for variant modeling
  • Shows a branch structure that the compiler can understand
  • Explains how this replaces Python-style implicit key conventions

Common mistakes

  • Using one giant optional-field object instead of a union of legal variants.
  • Jumping straight to type assertions instead of narrowing from real checks.
  • Treating advanced typing as decoration rather than as a way to model dynamic data more honestly.

takeaways

  • Advanced TypeScript is not about cleverness. It is about making dynamic data explicit enough that refactors stay safe and callers stop guessing which fields are available in each branch.
  • A strong answer mentions that narrowing converts broad possibilities into safe concrete branches, making variant-heavy data easier to use than loosely typed dictionaries and assertions.
  • Uses discriminated unions for variant modeling