In a TypeScript project with `strictNullChecks` on, a value is declared `string[] | string | null`. Which branch of `if (typeof value === 'object')` does the checker put `null` in, what is the narrowed type there, and how do you exclude it?
answer
- the guard proves less than you assume
- null keeps company with the objects
- strictNullChecks is what reveals it
- pair the tag test with a null test
basics
~20 sNull lands in the true branch: the checker narrows to string[] | null, because the runtime check it models reports "object" for null. Combine the guard with an explicit non-null check, or test for the primitive member instead.
solid answer
~40 sThe checker models the runtime check faithfully, and that check does not separate `null` from real objects — so the true branch of `typeof value === 'object'` is narrowed to `string[] | null`, and the `else` branch gets `string`. Calling `value.length` in the true branch is then a compile error under `strictNullChecks`, which is the compiler doing its job rather than a bug. The usual fixes are to pair the guard with an explicit `value !== null` test, or to invert the logic and check the primitive member first, or — when you actually want the array — to use `Array.isArray(value)`, which excludes `null` on its own. With `strictNullChecks` off, `null` is not tracked in types at all, so the trap is invisible until it reaches production.
code
typescript · 13 linesdeclare const value: string[] | string | null;
if (typeof value === "object") {
// value: string[] | null — property access is an error here
}
if (value !== null && typeof value === "object") {
console.log(value.length); // value: string[]
}
if (Array.isArray(value)) {
console.log(value.length); // value: string[]
}go deeper
Know that a typeof object check does not rule out null, and that you normally write an explicit not-null test alongside it before touching any property.
Explain the mechanics: the branch keeps every constituent whose tag could be "object", null included, and strictNullChecks is what makes that visible in the reported type.
Demonstrate the judgment: pick the guard that proves what you need — a non-null test plus the tag check, or a more specific check such as Array.isArray — instead of silencing the error with an assertion.
Own the policy angle: strictNullChecks is the flag that makes this class of hazard a compile error at all, so treat its adoption, and any per-file escape hatch from it, as an explicit engineering decision with a migration plan.
## The situation ```ts declare const value: string[] | string | null; if (typeof value === "object") { // value: string[] | null value.length; // error under strictNullChecks: object is possibly null } else { // value: string } ``` The surprising part for most candidates is the true branch. It is not `string[]`. The checker keeps `null` there, and every property access in the branch is rejected. ## Why the checker does this A `typeof` guard is evidence, and the checker only credits it with what the underlying runtime check can actually prove. The runtime check reports the same tag for `null` as for ordinary objects — that is a JavaScript fact the type layer inherits rather than invents. Modelling it any other way would make the type system *lie*: it would report `string[]` for a value that can be `null`, and the resulting crash would be one the compiler had implicitly promised could not happen. So the true branch keeps every constituent whose tag could be `"object"`, and `null` is one of them. This is worth stating explicitly in an interview, because it inverts the usual complaint. The behaviour is not the checker being imprecise; it is the checker refusing to be more confident than the evidence allows. ## The role of strictNullChecks Everything above depends on `strictNullChecks` being on. With it off, `null` and `undefined` are members of every type and are not tracked separately, so the declared type effectively collapses, the true branch reports `string[]`, and nothing complains — right up until the property access throws at runtime. This is the clearest small demonstration of what that flag buys: it is what makes the `null` in the object branch visible at all. ## The three fixes **Add the explicit non-null test.** The idiomatic form puts it first, so the guard reads as one unit: ```ts if (value !== null && typeof value === "object") { value.length; // value: string[] } ``` **Invert the check.** If the union's non-object member is the interesting one, test for it and let the else branch do the work — but note the else branch still contains `null`, so this only helps when you are handling the primitive: ```ts if (typeof value === "string") { value.toUpperCase(); // value: string } ``` **Use a more specific guard.** When what you actually want is the array, `Array.isArray(value)` narrows directly to the array constituent and leaves `null` in the false branch, because its declared predicate says nothing about null: ```ts if (Array.isArray(value)) { value.length; // value: string[] } ``` ## Starting from unknown The same rule shows up in a second place. When the starting type is `unknown` there is no union to filter, so the guard produces a type — and it produces `object | null`, not `object`: ```ts function f(v: unknown) { if (typeof v === "object") { // v: object | null } } ``` This is why the standard shape for validating an unknown payload is `typeof v === "object" && v !== null`, and why skipping the second half produces an error on the very first property access. ## What the guard does not remove One more consequence: the object branch keeps *all* object-like constituents. Arrays, class instances, plain objects and boxed primitives all report the object tag, so a `typeof` guard can never pick one interface out of a union of interfaces. That job belongs to a different guard — a property-presence check, a discriminant, or `instanceof` — and a candidate who reaches for `typeof` to separate two object shapes has misread what the check can prove. ## Interview framing Name the branch (`null` goes with the objects), state the narrowed type, say that `strictNullChecks` is what makes it visible, and give the fix in one breath. The strongest version adds *why*: narrowing credits a guard only with what the runtime check can actually establish, and this check cannot separate null from an object.
- Why is the compiler's behaviour here the right design rather than an annoying imprecision?Because narrowing must never claim more than the guard establishes. The runtime check genuinely cannot separate null from an object, so reporting `string[]` would be a promise the type system could not keep, and the resulting crash would happen in code the compiler had blessed. Keeping null in the branch converts a would-be runtime failure into a compile error at the exact line that is unsafe.
- What does the same guard narrow an `unknown` value to, and what is the idiomatic validation shape?It narrows to `object | null` — again because the check cannot rule out null. The idiomatic shape is therefore `typeof v === 'object' && v !== null`, after which property presence can be tested. Skipping the second half fails on the first property access, which is the compiler flagging the missing half of the check rather than obstructing you.
- If the union is `string[] | Date | null`, does adding the non-null test give you a usable type?It gives you `string[] | Date`, which is honest but usually not yet usable: the two share almost no members. You then need a second guard to separate them — `Array.isArray` for the array constituent, or `instanceof Date` for the other. A `typeof` guard can never split two object types, since both report the same tag.
saying these in an interview costs you the question
- Assumes the object branch already excludes null
- Says the error is a compiler bug rather than a real hazard
- Thinks disabling strictNullChecks fixes the problem
- Uses a non-null assertion instead of an actual null check
- Expects typeof to separate two object shapes in a union