Lesson 3 of 80
A union type can't be used as any specific member until you prove which one it is — and checking one shared property can narrow an entire object at once.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Inside `if (res.status === "success") { ... }`, why does res.data become accessible in that branch, when data isn't even part of the shared status check?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A value typed string | number can't be treated as either until TypeScript can prove which one it actually is in that branch of code. That proof comes from narrowing — runtime checks like typeof, instanceof, in, or a truthy check, each of which TypeScript's control-flow analysis understands and uses to shrink the type within that branch.
A discriminated union is the most powerful version of this: a union of object shapes that all share one common property with a distinct literal value (the "discriminant" or "tag"). Checking that one property narrows the entire object.
type ApiResponse =
| { status: "success"; data: string }
| { status: "error"; message: string };
function handle(res: ApiResponse) {
if (res.status === "success") {
console.log(res.data); // ✅ TS knows this branch is the success shape
} else {
console.log(res.message); // ✅ narrowed to the error shape here
}
}
Checking res.status doesn't just narrow status itself — it narrows res's entire type, which is why res.data becomes accessible in one branch and res.message in the other, with no manual casting anywhere.
A discriminated union: two object shapes sharing one common property, status, with different literal values.
res.status === "success" and then reference res inside a setTimeout callback, TypeScript can't prove res hasn't changed by the time that callback runs — the narrowing doesn't carry through into a deferred function.in operator narrows without a dedicated tag field. if ("data" in res) narrows based on which property actually exists, useful when the union wasn't designed with an explicit discriminant.switch handling that union silently misses it — without a never check, that's a runtime gap; with one, it's a build failure pointing at the exact spot.string | number narrows fine with typeof, but there's no shared "tag" property — the discriminated-union pattern specifically applies to unions of object shapes with a common literal property.Modeling state as a discriminated union ({status: "loading"} | {status: "success", data} | {status: "error", error}) instead of a pile of optional/possibly-undefined fields is one of the clearest signals of TypeScript fluency — it makes invalid states (like data and error both being set) impossible to represent at all, not just unlikely.