In TypeScript, given `const names: (string | undefined)[]`, what type does `names.filter(n => n !== undefined)` produce, and how do you guarantee the result is `string[]` regardless of the compiler version?
answer
- two overloads, one takes a predicate
- boolean callback keeps the element type
- the compiler learned to infer predicates
- name the guard, do not rely on inference
- Boolean as a callback proves nothing
basics
~20 sOn TypeScript 5.5 and later the compiler infers a type predicate for that arrow and the result is string[]; on earlier versions it stays (string | undefined)[]. Passing a function declared with a value is string return type always gives string[].
solid answer
~50 s`Array.prototype.filter` has two overloads: one takes a callback typed `(value: T, index: number, array: T[]) => value is S` with `S extends T` and returns `S[]`; the other takes a plain boolean-ish callback and returns `T[]`. Which one applies depends entirely on whether the callback's type is a predicate. Before TypeScript 5.5, an inline arrow returning a comparison was just `boolean`, so the second overload won and the result stayed `(string | undefined)[]` — the classic annoyance. TypeScript 5.5 infers a predicate for an unannotated function whose body is a single return that narrows its parameter, so the same arrow now selects the first overload and yields `string[]`. To be version-proof, pass a named guard declared `function isDefined(v: string | undefined): v is string`, and never rely on `filter(Boolean)`, which does not narrow at all.
code
typescript · 13 linesconst names: (string | undefined)[] = ["a", undefined, "b"];
// Version-proof: the callback's declared type is a predicate.
function isDefined(value: string | undefined): value is string {
return value !== undefined;
}
const defined: string[] = names.filter(isDefined);
console.log(defined.map((s) => s.toUpperCase()));
// Does NOT narrow on any version: Boolean is a plain boolean function.
const stillWide: (string | undefined)[] = names.filter(Boolean);
console.log(stillWide.length);go deeper
Know the symptom and the safe habit: after filtering out undefined, the elements may still be typed as possibly undefined unless the callback is a declared guard, and filter(Boolean) does not fix it.
Explain the mechanism — filter is overloaded, and the predicate overload only matches when the callback's type carries value is S — and note that TypeScript 5.5 can infer that for an unannotated arrow.
Show awareness of the version boundary in practice: pin the compiler, prefer declared guards over inferred ones, and treat a silently widened array as a signal that someone annotated a guard as boolean.
Weigh whether the codebase should depend on inference at all: inferred predicates make behaviour a function of the toolchain version, so decide the standard, encode it in lint or review, and keep published .d.ts output deterministic.
## The two overloads that decide everything In TypeScript's bundled library declarations, `filter` on an array of `T` is overloaded. The first overload is the one you want: ```ts filter<S extends T>( predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any ): S[]; ``` It only matches when the callback you pass has a *type predicate* in its type. The second overload accepts any callback returning a boolean-ish value and gives back `T[]` — same element type in, same element type out. So the entire question of whether `filter` narrows reduces to: is my callback's type a predicate, or plain `boolean`? Note the constraint `S extends T`: the type you narrow to must be a subtype of the element type. You can filter `(string | undefined)[]` down to `string[]`; you cannot filter it into `number[]`. ## Why the inline arrow used to lose ```ts const names: (string | undefined)[] = ["a", undefined, "b"]; const defined = names.filter(n => n !== undefined); ``` The arrow's inferred return type used to be simply `boolean`. `boolean` is not a predicate, the first overload did not match, and `defined` came out as `(string | undefined)[]` — so `defined[0].toUpperCase()` still errored, which is one of the most-reported TypeScript papercuts of the pre-5.5 era. ## What TypeScript 5.5 changed TypeScript 5.5 added **inferred type predicates**. When a function has no return type annotation and its body is (in effect) a single `return` of an expression that narrows one of its parameters — and that parameter is never reassigned — the compiler infers `param is T` instead of `boolean`. The arrow above therefore gets the type `(n: string | undefined) => n is string`, the predicate overload matches, and `defined` is `string[]` with no extra code. Two consequences follow. First, the same source can typecheck differently on 5.4 and 5.6, which matters when a library ships `.d.ts` files or a monorepo pins different compiler versions. Second, the inference is *fragile by construction*: add a second return statement, assign to the parameter, or annotate `: boolean`, and it silently reverts to a plain boolean and your array widens again without any error to tell you. ## The version-proof form Declare the guard explicitly and pass it by reference: ```ts function isDefined(value: string | undefined): value is string { return value !== undefined; } const defined: string[] = names.filter(isDefined); ``` Now the callback's *declared* type is a predicate on every compiler version, the first overload matches, and the narrowing is stated rather than inferred. A reader of the call site can also see what is being asserted without opening the arrow body. ## Traps around this pattern **`filter(Boolean)` does not narrow.** `Boolean` is declared as an ordinary function returning `boolean`, not a predicate, so `names.filter(Boolean)` keeps `(string | undefined)[]`. It is popular precisely because it looks like it should work; it does not, and no compiler version changes that. **Annotating the guard as `: boolean` defeats it.** People sometimes "tidy up" a guard by adding an explicit return type. If they write `: boolean` instead of `: value is string`, every call site quietly stops narrowing. **The asserted type must satisfy `S extends T`.** A guard declared over a wider parameter type than the array's element type will not match the predicate overload cleanly; keep the guard's parameter type aligned with the element type you are filtering. **Other array methods share the pattern.** `find` also has a predicate overload — `find<S extends T>(predicate: (value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined` — so a named guard improves `find` results in the same way, returning `S | undefined` rather than `T | undefined`. ## What to say in an interview Name the mechanism, not just the workaround: `filter` is overloaded on whether the callback is a predicate, an inline arrow only produces one under TypeScript 5.5's inference, and an explicitly declared guard produces one always. That answer covers both the old codebases and the new ones.
- Why does `names.filter(Boolean)` still leave `undefined` in the element type?Because `Boolean` is declared as an ordinary function returning `boolean`, not as a type predicate. `filter` therefore matches its non-predicate overload and returns the same element type it was given. The values really are removed at runtime; only the type is unchanged.
- Does `find` benefit from the same trick?Yes. `find` is overloaded the same way: pass a callback whose type is `value is S` and the result is `S | undefined` instead of `T | undefined`. So `items.find(isDefined)` gives `string | undefined` rather than the wider element union.
- A teammate adds an explicit `: boolean` return type to a working guard. What breaks?Every call site quietly stops narrowing — filtered arrays widen back to the element union and `if (guard(x))` no longer refines `x`. Nothing errors at the guard itself, so the failures surface as unrelated type errors elsewhere, or as none at all if assertions hide them.
- Why might the same file typecheck differently on two machines?If they run different TypeScript versions across the 5.5 boundary, an unannotated callback is a predicate on one and plain `boolean` on the other. Pin the compiler version in the repo and annotate guards explicitly so the result does not depend on it.
saying these in an interview costs you the question
- Claims filter always preserves the array's element type
- Believes filter(Boolean) removes undefined from the type
- Adds a type assertion on the result instead of typing the callback
- Thinks the guard must be generic to work with filter
- Says the array narrows because values were removed at runtime