In TypeScript, what type does `if (Array.isArray(x))` narrow `x: unknown` to, and why is that a hole in an otherwise strict codebase?
answer
- look at how the library declares it
- the predicate is wider than you want
- elements come back unchecked
- no strictness flag reports a library any
basics
~20 sIt narrows to any[], because the standard library declares the check with the predicate arg is any[]. Every element read out of it is any, so a strict codebase silently loses type safety inside the branch.
solid answer
~40 sThe standard library declares the check as `isArray(arg: any): arg is any[]`, so narrowing an `unknown` with it produces `any[]` — an array whose elements are `any`. Inside the branch, `x[0]` and everything derived from it type-check against anything, and no strictness flag catches it, because the `any` comes from a library declaration rather than from an implicit annotation in your code. That is the hole: the guard that was supposed to make an untrusted value safe hands you the least safe element type there is. Narrowing a *union* is unaffected — for `string[] | string` the guard filters to `string[]` normally. For unknown input, wrap it: a one-line predicate returning `x is unknown[]` keeps the array-ness and forces you to check the elements before using them.
code
typescript · 17 linesfunction handleLoose(x: unknown) {
if (Array.isArray(x)) {
// x: any[] — element access is unchecked from here on
return x[0];
}
return undefined;
}
function isUnknownArray(x: unknown): x is unknown[] {
return Array.isArray(x);
}
function handleStrict(x: unknown): string[] {
if (!isUnknownArray(x)) return [];
// x: unknown[] — each element must still be checked
return x.filter((item): item is string => typeof item === "string");
}go deeper
Know that this is the correct way to test whether a value is an array, that a typeof check cannot do it, and that it narrows a union such as string-or-array to the array member.
Explain that the standard library declares it with a predicate of any-array, so narrowing an unknown yields any-array and element access stops being checked.
Demonstrate the habit of reading library type declarations rather than assuming, and wrap the guard in a predicate returning unknown-array so untrusted input still forces a per-element check.
Own where any is permitted to enter the system at all: treat library-sourced any as a boundary defect, and standardise on validated parsing at the edge so ad-hoc guards are not the last line of defence.
## The declaration behind the behaviour The built-in check is declared in the standard library as a type predicate: ```ts interface ArrayConstructor { isArray(arg: any): arg is any[]; } ``` Narrowing always produces the predicate's stated type, so applying it to an `unknown` gives `any[]`: ```ts function handle(x: unknown) { if (Array.isArray(x)) { // x: any[] x[0].whatever.deeply.nested; // compiles, no complaint } } ``` The branch is exactly where a careful codebase most wants precision — the value came from outside and has just been established to be an array — and it is the one place the checker stops helping. ## Why no flag catches it The strictness flags that police `any` are about *implicit* occurrences: a parameter or variable with no annotation that the compiler had to fall back on. Here the `any` is explicit, correct as a declaration, and lives in the library's own type definitions. Nothing in your source has an unannotated position, so nothing is reported. The `any` propagates outward from the branch through every expression derived from an element, and each of those expressions also stops being checked. This is the general shape of the `any` hazard — it is contagious, and one entry point disables checking for everything downstream of it. Why is the library declared that way? Because the check genuinely proves only array-ness. `unknown[]` would be the honest element type, but it would break a great deal of existing code that reads elements straight out of the narrowed value, so the declaration favours compatibility. ## Unions are the easy case When the input is a union, the guard behaves like any other predicate and simply filters the constituents: ```ts function lines(v: string[] | string): string[] { if (Array.isArray(v)) { return v; // v: string[] — element type preserved } return v.split("\n"); // v: string } ``` The element type survives because narrowing keeps the declared constituent rather than replacing it with the predicate's type. So the trap is specific to narrowing from `unknown` or `any` — that is, exactly the untrusted-input case. ## The fix Wrap the check once, in a helper whose predicate tells the truth: ```ts function isUnknownArray(x: unknown): x is unknown[] { return Array.isArray(x); } function handle(x: unknown) { if (isUnknownArray(x)) { // x: unknown[] for (const item of x) { if (typeof item === "string") { // item: string } } } } ``` Now the compiler forces a per-element guard before anything is used, which is the check you should have been writing anyway. The same wrapper is what a validation library gives you at its boundary; if the codebase already runs a schema validator on inbound data, that layer supersedes this one entirely. ## The other half: what the guard proves The check establishes array-ness and nothing else. It says nothing about length, about element types, or about whether the array is homogeneous. It also excludes `null` on its own, which makes it a cleaner alternative to an object-tag test when what you actually want from a nullable union is the array member. And, like every guard here, it is erased in one direction only: the call itself is real JavaScript that survives compilation, while the narrowed type is bookkeeping that ends at emit. ## Interview framing Name the declared predicate, state that narrowing an `unknown` therefore yields `any[]`, and explain why no flag reports it — that is the part that separates a candidate who has read the library declarations from one who has only used the API. Then give the one-line wrapper as the fix, and note that narrowing a union was never affected.
- Why does no strictness flag report the `any` you get from this guard?Because those flags target *implicit* occurrences — positions in your own code that the compiler had to fall back on because there was no annotation. Here the `any` is explicit and comes from the standard library's own declaration, so there is nothing unannotated to report. It then propagates through every expression derived from an element, silently disabling checking downstream.
- Does the same problem appear when narrowing `string[] | string`?No. With a declared union the guard filters constituents rather than substituting the predicate's type, so the branch is `string[]` with its element type intact. The hazard is specific to starting from `unknown` or `any` — which is unfortunately exactly the untrusted-input case where you reached for the guard in the first place.
- When the value is a nullable union, why might you prefer this guard over a `typeof` object test?Because the object tag test keeps `null` in the true branch — the underlying runtime check cannot separate them — so you need a second condition before touching any property. This guard's predicate matches only arrays, so `null` lands in the false branch and the true branch is immediately usable.
saying these in an interview costs you the question
- Assumes the narrowed element type is unknown rather than any
- Expects noImplicitAny to flag the resulting any
- Thinks the check also proves the elements share a type
- Believes the guard is special-cased in the compiler rather than declared in the library
- Uses an assertion to "fix" the element type instead of checking elements