TypeScript narrowing patterns I actually use
แพตเทิร์นการ narrow ใน TypeScript ที่ผมใช้จริง
There are dozens of TypeScript narrowing tricks. I use five of them every day. The rest live in blog posts I bookmarked once and never opened again.
The thing nobody tells you when you start with TypeScript is that 90% of the real work isn't typing things — it's narrowing them. You usually know what the type could be. The interesting part is convincing the compiler what it is, right here, on this line.
1. Discriminated unions
This is the one I reach for first, every time. If a value can be in more than one shape, give each shape a literal tag. The compiler does the rest.
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error };
function unwrap<T>(r: Result<T>): T {
if (r.ok) return r.value; // narrowed
throw r.error; // narrowed the other way
}
Once you start seeing your domain as a union of cases tagged by a literal, half your runtime checks become compile-time checks. That's the win.
2. in for shape checks
When you don't own the types and can't add a discriminator, fall back to
in. It's pragmatic, not pretty.
function area(s: { width: number; height: number } | { radius: number }) {
if ("radius" in s) return Math.PI * s.radius ** 2;
return s.width * s.height;
}
I reach for this most often when I'm bridging two libraries that don't know about each other and I just need a quick branch.
3. Type predicates
When the narrowing logic isn't trivial — say, validating something parsed
from an API — wrap it in a function that returns value is X. The function
becomes a contract: "if I return true, this value is that type."
function isUser(x: unknown): x is { id: string; name: string } {
return typeof x === "object" && x !== null && "id" in x && "name" in x;
}
The trap with predicates is they're only as honest as you write them. The compiler trusts you. Lie once and the bug will surface a hundred lines away.
4. Assertion functions
Same shape as predicates, but they throw instead of returning a boolean. Useful at boundaries where you'd rather fail loudly than carry a wrong assumption deeper into the system.
function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new Error("not a string");
}
5. satisfies
The newest of the five, and the one I find myself reaching for more and more. It checks a value against a type without widening it.
const config = {
host: "localhost",
port: 5432,
} satisfies { host: string; port: number };
// config.host is "localhost", not string
The classic case is a config object: you want the structural check, but
you also want to keep the literal types so you can use them later. as
gives you the check at the cost of the inference. satisfies gives you
both.
If you only learn the first two, you'll get 80% of the mileage. Add
satisfies and you'll cover most of what's left. The other two are nice
when you need them and invisible when you don't — which is honestly the
highest praise I can give a TypeScript feature.