You tried to push into an array or call a function and got:
src/tags.ts(2,17): error TS2345: Argument of type '"urgent"' is not assignable to parameter of type 'never'.never is the type with no values. If a parameter’s type is never, nothing you pass will ever be accepted, so the problem is almost never the argument. It is the parameter: somewhere the compiler concluded that the set of allowed values is empty. Older 5.x releases print the widened type (Argument of type 'string') for a string literal argument; TypeScript 5.9 and 7.0 print the literal itself ('"urgent"'). Passing a string variable gives the familiar Argument of type 'string' is not assignable to parameter of type 'never' in every version.
Quick fix checklist
- Hover the method or function you are calling and find the
neverin its signature:never[], an intersection, or a narrowed variable. pushonnever[]: the array was created as[]with no element type. Annotate it (tags: string[]), type the containing object, or pass a type argument such asreduce<number[]>(..., [])oruseState<string[]>([]).- Calling
handlers[key](value)wherekeyis a union: rewrite with a genericK extends keyof Tand a mapped type so key and value stay correlated. assertNever(x)failing: a case is missing in theswitch, or the discriminant is typedstringinstead of a union of literals.- Never fix it with
value as neverorany[].
Before you start
The examples use strict: true and were compiled with TypeScript 7.0 and 5.9, which print identical messages. Know whether noImplicitAny is on, because it changes how a local const list = [] is inferred (see below). The same root cause reported in an assignment rather than a call is TS2322 (Type 'string' is not assignable to type 'never'); everything here applies to it too.
Why it happens
There are three common routes to a never parameter.
Empty arrays with nothing to infer from. In { title: "Notes", tags: [] } the literal [] has no elements, so TypeScript cannot infer an element type and, under strictNullChecks, picks never[]. That is sound (an empty array contains no values) but useless, because push(item: never) then rejects everything. A local variable is the exception: with noImplicitAny on, const out = [] becomes an “evolving” array whose type grows with each push, so it works. Turn noImplicitAny off while keeping strictNullChecks on and that local becomes never[] as well.
Calling a union of functions. If fn is ((v: string) => void) | ((v: number) => void), you don’t know which function you hold, so a safe argument must be acceptable to both: string & number. No value is both, so the intersection is never. The same thing happens with validators[field](value) when field is a union of keys, because indexing an object with a union key gives you a union of functions.
Narrowing that leaves nothing. After a switch that handles every member of a union, the variable’s type in the default branch is never. Exhaustiveness helpers rely on this: assertNever(value: never) only compiles when nothing is left. If something is left, you get TS2345 naming it.
Step-by-step walkthrough
Step 1: Reproduce and find the source of never
const draft = { title: "Release notes", tags: [] };
draft.tags.push("urgent");
class Inbox {
messages = [];
add(message: string) {
this.messages.push(message);
}
}
export const evens = [1, 2, 3, 4].reduce((acc, n) => {
if (n % 2 === 0) acc.push(n);
return acc;
}, []);src/tags.ts(2,17): error TS2345: Argument of type '"urgent"' is not assignable to parameter of type 'never'.
src/tags.ts(7,24): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
src/tags.ts(12,29): error TS2345: Argument of type 'number' is not assignable to parameter of type 'never'.Hover draft.tags, this.messages and acc: each one shows never[]. The error is reported at the push, but the cause is the line that created the array.
Step 2: Give empty arrays an element type
interface Draft { title: string; tags: string[] }
const draft: Draft = { title: "Release notes", tags: [] };
draft.tags.push("urgent");
class Inbox {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
}
export const evens = [1, 2, 3, 4].reduce<number[]>((acc, n) => {
if (n % 2 === 0) acc.push(n);
return acc;
}, []);Typing the object through an interface is best when the shape is used elsewhere. For a one-off, tags: [] as string[] is acceptable: it is an assertion, but asserting that an empty array holds strings can never be false. For React, useState([]) infers never[] the same way; write useState<string[]>([]).
Step 3: Correlate keys and values instead of calling a union
interface FieldValues { email: string; age: number }
const validators = {
email: (value: string) => value.includes("@"),
age: (value: number) => value >= 18,
};
export function validate(field: keyof FieldValues, value: string | number) {
return validators[field](value);
}src/validate.ts(9,28): error TS2345: Argument of type 'string | number' is not assignable to parameter of type 'never'.
Type 'string' is not assignable to type 'never'.The signature allows validate("age", "hello"), so the compiler is right to refuse. Tie the value type to the key with a mapped type and a generic:
type Validators = { [K in keyof FieldValues]: (value: FieldValues[K]) => boolean };
const validators: Validators = {
email: (value) => value.includes("@"),
age: (value) => value >= 18,
};
export function validate<K extends keyof FieldValues>(field: K, value: FieldValues[K]) {
return validators[field](value);
}Now validators[field] has type Validators[K], which TypeScript can call with FieldValues[K]. Callers get checked too: validate("age", "21") is rejected.
Step 4: Read exhaustiveness errors as a to-do list
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "triangle"; base: number; height: number };
function assertNever(value: never): never {
throw new Error(`Unhandled shape: ${JSON.stringify(value)}`);
}
export function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
default: return assertNever(shape);
}
}src/shapes.ts(14,33): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.This is the check doing its job: the argument type is the variant you did not handle. Add case "triangle". If instead the message says Argument of type 'Shape', the whole type survived the switch, which means narrowing never happened. The usual cause is a discriminant typed as kind: string rather than a union of literals, or switching on a different variable from the one you pass to assertNever.
Worked scenario
An upload widget keeps its state in a plain object, inferred from the initial value:
const initialState = { loading: false, errors: [] };
type UploadState = typeof initialState;
export function recordError(state: UploadState, message: string) {
state.errors.push(message);
state.loading = false;
}src/upload.ts(5,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.Diagnosis: deriving the type with typeof initialState is convenient, but the initial value has an empty array, so UploadState["errors"] is never[]. Redux Toolkit’s createSlice infers state from initialState the same way, which is why typing the initial state explicitly is the standard advice there. The fix is to write the type first and make the initial value conform to it:
export interface UploadState {
loading: boolean;
errors: string[];
}
export const initialState: UploadState = { loading: false, errors: [] };
export function recordError(state: UploadState, message: string) {
state.errors.push(message);
state.loading = false;
}Common mistake
The fix most often copied from forums is state.errors.push(message as never). It compiles because never is assignable to everything, but the array is still typed never[]. Every element you read is never: state.errors[0].toUpperCase() fails with Property 'toUpperCase' does not exist on type 'never', while const first: number = state.errors[0] compiles without complaint. errors: [] as any[] swaps one hole for another.
For the function-union case, people write (validators[field] as any)(value) or widen each validator to (value: any) => boolean. Both bring back validate("age", "hello"), which is exactly the bug the compiler was pointing at. For exhaustiveness, deleting the default branch to silence the error removes the safety net for the next variant someone adds.
Verify the behavior
Run npx tsc --noEmit and expect exit status 0. Keep a type-level test that proves the correlated signature still rejects bad pairs:
validate("email", "a@b.co");
validate("age", 21);
// @ts-expect-error: age needs a number
validate("age", "21");For the state fix, a runtime test confirms the behaviour (Node 22.18+ runs .ts test files directly):
import { test } from "node:test";
import assert from "node:assert/strict";
import { initialState, recordError, type UploadState } from "./upload.ts";
test("records an error and stops loading", () => {
const state: UploadState = { ...initialState, errors: [], loading: true };
recordError(state, "File too large");
assert.deepEqual(state, { loading: false, errors: ["File too large"] });
});node --test src/upload.test.ts should report pass 1.
Interview exercise
Given declare const handler: ((value: string) => void) | ((value: number) => void);, why does handler("hello") fail with a never parameter, and how would you design the code so the call is safe?
Answer and reasoning
Function parameters are contravariant. Holding a union of functions means you don’t know which function you will call, so the argument must be valid for every member: string and number at once. The compiler computes the parameter as the intersection string & number, which is empty, so it reduces to never and rejects "hello". This is correct: if handler were the number version, passing a string would be a bug.
To make the call safe, keep the information that tells you which function you hold. Options: a discriminated union ({ kind: "text"; run: (v: string) => void } | { kind: "count"; run: (v: number) => void }) and narrow on kind before calling; or a generic map keyed by event name, as in the validate example, so the key determines the parameter type. A strong answer explains the intersection rule rather than reaching for a cast, and notes that as never silences the error without making the call any safer.