Narrow a Union With typeof and in
Use typeof, in and Array.isArray on three real unions, and avoid the two mistakes that stop narrowing from working. After this, you know which check fits which union.
A union is only useful once you can find out which member you have. This page covers the two checks you will use most, typeof for simple values and in for objects, with three real cases and the two mistakes that make narrowing fail.
What typeof can tell apart
typeof returns one of eight words.
| typeof gives | Use it for |
|---|---|
| "string", "number", "boolean", "bigint", "symbol" | plain values |
| "undefined" | a missing value |
| "function" | a callback or a function option |
| "object" | objects, arrays and null (not helpful on its own) |
Case 1: a string or a list of strings
An option that accepts one tag or several.
function toList(tags: string | string[]): string[] {
if (typeof tags === "string") {
return [tags];
}
return tags;
}
console.log(toList("new"));
console.log(toList(["new", "sale"]));
[ 'new' ]
[ 'new', 'sale' ]
Inside the if, tags is a string. After it, the only member left is string[]. No cast was needed.
Case 2: a value or a function that produces it
function resolve(value: string | (() => string)): string {
if (typeof value === "function") {
return value();
}
return value;
}
console.log(resolve("fixed"));
console.log(resolve(() => "computed"));
fixed
computed
typeof value === "function" is true only for the function member, so the call value() is allowed inside the block.
Case 3: objects with different keys, using in
typeof says "object" for all of these, so it cannot help. Ask whether a property is there.
type Failure = { error: string };
type Success = { items: string[] };
function show(result: Success | Failure): string {
if ("error" in result) {
return "Failed: " + result.error;
}
return result.items.length + " items";
}
console.log(show({ items: ["a", "b"] }));
console.log(show({ error: "timeout" }));
2 items
Failed: timeout
"error" in result is true only for a Failure, so the compiler knows the rest is a Success.
Mistake 1: using in with a key both members have
type Customer = { name: string; id: number };
type Guest = { name: string; visits: number };
function describe(person: Customer | Guest): string {
if ("name" in person) {
return "Customer ID " + person.id;
}
return "Guest";
}
both.ts(6,36): error TS2339: Property 'id' does not exist on type 'Customer | Guest'.
Property 'id' does not exist on type 'Guest'.
Both types have name, so the check proves nothing, and the compiler does not narrow. Pick a key that exists in only one member, like "id" in person. If no such key exists, the types are too alike for in, and you will want a field that names the kind, like kind: "customer".
Mistake 2: using typeof to tell an array from an object
console.log(typeof [], typeof {}, typeof null);
console.log(Array.isArray([]), Array.isArray({}));
object object object
true false
All three typeof results are "object". To check for an array, use Array.isArray. TypeScript understands it, and narrows to an array type inside the block.
function first(input: string[] | { name: string }): string {
if (Array.isArray(input)) {
return input[0] ?? "empty";
}
return input.name;
}
console.log(first(["a", "b"]), first([]), first({ name: "Ana" }));
a empty Ana
⚠ Warning: Narrowing only lasts inside the block you checked. Move the use of the value outside that block and the compiler forgets what you proved.
Which check to use
- Simple values and functions:
typeof. - Arrays:
Array.isArray. - Objects that differ by a key:
in, with a key only one member has. null: compare with=== null, never withtypeof.
Related course: TypeScript for Beginners.
About
Use typeof, in and Array.isArray on three real unions, and avoid the two mistakes that stop narrowing from working. After this, you know which check fits which union.
- Responsible
- Christopher R
- Last Update
- 10/14/2026
- Reading Time
- 3 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.