Refining unions across function boundaries
Carry safe narrowing across function boundaries using discriminated unions, predicate returns, and Result-style types.
Refining unions across function boundaries is a free TypeScript Academy lesson on CoddyKit — lesson 3 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Intro
Goal: Keep your narrowing intact across function calls using tagged unions, predicate returns, and validation helpers.
- Zero unsafe
asat call sites - Refinements travel with data
Tagged union return
Result pattern: caller narrows by checking ok. No casts, clear control flow.
type Ok<T> = { ok: true; value: T }
type Err = { ok: false; error: string }
type Result<T> = Ok<T> | Err
function parseIntSafe(s: string): Result<number> {
const n = Number(s)
return Number.isFinite(n) ? { ok: true, value: n } : { ok: false, error: "NaN" }
}
function useIt(s: string) {
const r = parseIntSafe(s)
if (r.ok) {
// r is Ok<number>
return r.value * 2
}
// r is Err
return `bad: ${r.error}`
}Predicate return
Predicate returns (x is T) centralize checks and keep the refinement at call sites.
type User = { id: number; name: string }
type MaybeUser = User | null
function hasUser(x: MaybeUser): x is User {
return x !== null
}
function greet(x: MaybeUser) {
if (hasUser(x)) {
// x narrowed to User
return `Hello ${x.name}`
}
return "Anonymous"
}Forwarding refinement
Refine with a guard (e.g., isCircle), then pass the value forward; downstream functions see the narrower type.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
function isCircle(s: Shape): s is Extract<Shape, { kind: "circle" }> {
return s.kind === "circle"
}
function area(s: Shape) {
if (isCircle(s)) {
// s is circle here
return Math.PI * s.radius * s.radius
}
return s.size * s.size
}Validate at boundary
Validate at the boundary and return Valid/Invalid; callers narrow by the tag and get fully typed data.
type Payload = { email: string; retries?: number }
type Valid = { ok: true; data: Required<Payload> }
type Invalid = { ok: false; issues: string[] }
type Validation = Valid | Invalid
function validate(p: unknown): Validation {
const issues: string[] = []
if (typeof p !== "object" || p === null) return { ok: false, issues: ["not an object"] }
const x = p as any
if (typeof x.email !== "string") issues.push("email:string")
if (x.retries !== undefined && typeof x.retries !== "number") issues.push("retries:number")
if (issues.length) return { ok: false, issues }
return { ok: true, data: { email: x.email, retries: x.retries ?? 0 } }
}
function handle(p: unknown) {
const v = validate(p)
if (!v.ok) return `bad: ${v.issues.join(",")}`
// v.data is fully typed here
return `send to ${v.data.email} (${v.data.retries})`
}Tips
Best practices:
- Prefer small, stable tags (ok, kind)
- Centralize checks in predicate helpers
- Use exhaustive switches at call sites
- Avoid
anyand unsafe casts
Cross-boundary narrowing
Quick check: Which pattern best preserves narrowing across function boundaries?
Recap
Recap: Carry refinements as data (Result), validate at boundaries, and avoid unsafe casts; callers narrow via simple tag checks.
Frequently asked questions
Is the “Refining unions across function boundaries” lesson free?
Yes — the full text of “Refining unions across function boundaries” is free to read here on the web, and the TypeScript Academy course includes 3 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.
What will I learn in “Refining unions across function boundaries”?
Carry safe narrowing across function boundaries using discriminated unions, predicate returns, and Result-style types. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start TypeScript Academy?
No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 3, so you can start here or from the beginning and move at your own pace.
How long does the “Refining unions across function boundaries” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this TypeScript Academy lesson?
Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Exhaustive switches & never checks
- Predicate functions & satisfies operator
- Refining unions across function boundaries