In TypeScript, `declare const xs: string[] | number[];` and then `xs.push("a")` fails with "Argument of type 'string' is not assignable to parameter of type 'never'". Why does `never` appear in that message, and how would you fix it?
answer
- look at the receiver, not the argument
- the union is on the array, not the element
- signatures combine when you call through a union
- parameters intersect, results union
- string & number is empty
basics
~20 sCalling a method on a union of array types combines the candidate signatures, and the parameter types intersect: string & number, which is never. No argument satisfies both element types, so every call is rejected. Fix by widening the element type or narrowing the array first.
solid answer
~50 sThe `never` is not coming from your argument — it comes from the receiver. `xs` is a union of two array types, so `xs.push` is a union of two methods: one taking `string` and one taking `number`. To call through a union, the compiler builds a single signature whose parameter is the **intersection** of the candidates, because the argument has to be valid for whichever member `xs` really is. `string & number` has no members, so it reduces to `never`, and nothing is assignable to `never` — hence the message. Two real fixes: if the array genuinely holds mixed values, declare it `(string | number)[]`, which has a `push` taking `string | number`; if it is genuinely one or the other, narrow `xs` with a type guard or a discriminated wrapper before mutating. Asserting the argument `as never` compiles, but it just deletes the check the compiler was performing.
code
typescript · 15 linesdeclare const xs: string[] | number[];
// @ts-expect-error parameter is string & number, i.e. never
xs.push("a");
// fix 1: the element type is genuinely mixed
const mixed: (string | number)[] = [];
mixed.push("a");
mixed.push(1);
// fix 2: narrow the receiver first
declare function isStrings(a: string[] | number[]): a is string[];
if (isStrings(xs)) {
xs.push("a"); // xs is string[] here
}go deeper
Recognise that the never in this message comes from the type of the array, not from your argument, and that annotating the array as (string | number)[] is the usual fix.
Explain how calling a method through a union combines signatures: parameters intersect while return types union, and string & number reduces to never. Contrast the two array annotations precisely.
Diagnose it as a soundness result rather than a compiler quirk, and pick the fix that matches the data — widen, narrow with a guard, or restructure into a tagged union — while rejecting the as never escape hatch and saying why.
Own where these unions enter the system: API response models, heterogeneous records, over-broad shared types. Decide the codebase's convention for representing 'one of several homogeneous lists' so mutation sites are not where the model's flaw first surfaces.
## Read the error backwards The error names your argument (`'string'`) and a parameter type (`'never'`) you never wrote. The instinct is to look at the argument. The cause is on the other side of the dot: the *receiver* is a union type, and that is what produced the `never`. ```ts declare const xs: string[] | number[]; xs.push("a"); // Argument of type '"a"' is not assignable to parameter of type 'never'. ``` ## Why calling through a union intersects the parameters `xs` is either a `string[]` or a `number[]`; the type says the compiler does not know which. Accessing `.push` therefore yields a union of two method types: ``` ((item: string) => number) | ((item: number) => number) ``` To let you call that, TypeScript synthesises one signature from the members. The rule is driven by soundness: an argument must be acceptable to **every** member, because any of them might be the real one at runtime. "Acceptable to both `string` and `number`" is exactly `string & number` — the intersection — which reduces to `never`. Return types combine the other way (a union), since the result could come from either. So the parameter type `never` is the compiler saying, correctly: *there is no value I could let you push here that is safe for both possibilities*. If it allowed `"a"` and `xs` was actually the `number[]`, you would have corrupted the array with no error. The apparently absurd `never` is the compiler being right. ## The fixes, best first **1. The element type is genuinely mixed — say so.** ```ts const xs: (string | number)[] = []; xs.push("a"); // ok xs.push(1); // ok ``` `(string | number)[]` and `string[] | number[]` are different claims. The first is one array whose elements are each a string or a number. The second is one array that is *entirely* strings or *entirely* numbers. Most of the time a `string[] | number[]` annotation is simply the wrong model, written by reflex. **2. It really is one or the other — narrow before mutating.** A user-defined type guard collapses the union first, and after narrowing `push` has a concrete parameter type: ```ts declare function isStrings(a: string[] | number[]): a is string[]; if (isStrings(xs)) { xs.push("a"); // xs is string[] here } ``` **3. Carry the discriminant with the data.** If the two shapes mean different things, model them as a tagged union so narrowing is cheap and total: ```ts type Column = | { kind: "text"; items: string[] } | { kind: "number"; items: number[] }; declare const col: Column; if (col.kind === "text") { col.items.push("a"); // items is string[] } ``` **4. What not to do.** `xs.push("a" as never)` compiles. It also removes the only protection you had, and it will happily push a string into an array the rest of the program treats as numeric. Same for `(xs as any[]).push("a")`. Both convert a compile-time error into a runtime data bug, which is the wrong direction of trade. ## Reading `never` in error messages generally "not assignable to type 'never'" almost always means one of three things, and the fix follows from which: - **A union receiver.** As above: the parameter is an intersection of candidates. Narrow the receiver or widen the declared element type. - **An exhausted narrowing.** The value has been narrowed until nothing is left, so the compiler is telling you the branch is unreachable given the declared types — usually a sign that a union member was added or a guard is wrong. - **A collapsed computed type.** A generic, mapped or conditional type resolved to `never` somewhere upstream, and every use of it now rejects everything. Hover the intermediate aliases to find where it emptied out. In all three the message is a symptom; the cause is a type computed elsewhere. Chase the declaration, not the call. ## Why this shows up in real code Union-of-arrays annotations creep in from API layers ("this endpoint returns either a list of ids or a list of names"), from `Object.values()` over a heterogeneous record, and from over-eager `as const` inference. They are read-only-friendly — mapping and iterating over `string[] | number[]` mostly works, since those are covariant positions — and only break when you write. That is why the error tends to surface late, in the one place that mutates, long after the type was introduced.
- What is the difference between `string[] | number[]` and `(string | number)[]`?`string[] | number[]` says the array is homogeneous but you do not know which kind — all strings or all numbers. `(string | number)[]` says one array whose individual elements may each be a string or a number. Reading works for both; only the second lets you push either kind, because its element type genuinely admits both.
- Why do reading and iterating over `string[] | number[]` work when pushing does not?Reading is an output position: `xs[0]` and `xs.map` produce `string | number`, which is a perfectly usable union. Writing is an input position, and an input must be safe for whichever member the value really is — that requirement is the intersection of the candidate parameter types, which here is empty.
- Is `xs.push("a" as never)` ever a legitimate fix?No. It compiles because an assertion tells the checker to stop objecting, but it does not make the operation safe — if `xs` is really a `number[]`, you have just inserted a string into it and every later consumer is wrong. The assertion converts a compile-time error into a silent data corruption; fix the declared type instead.
saying these in an interview costs you the question
- Blames the argument instead of the union receiver
- Says push does not exist on a union type
- Silences it with `as never` or `as any[]`
- Thinks string[] | number[] means the same as (string | number)[]
- Assumes the array was inferred as never[] because it was empty