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