Question

Understanding "not assignable to parameter of type never" in TypeScript

Question

In TypeScript, why does this code produce the error Argument of type 'string' is not assignable to parameter of type 'never'?

const foo = (foo: string) => {
  const result = []
  result.push(foo)
}

I expected result to accept a string because foo is a string. What is TypeScript inferring here, why does this happen, and how should this code be written correctly? Is this a TypeScript bug or intended behavior?

Short Answer

By the end of this page, you will understand why TypeScript sometimes infers an empty array as never[], why that causes .push() to fail, and how to fix it by giving the array an explicit type or enough context for inference. You will also see how this fits into TypeScript's type inference rules in real projects.

Concept

TypeScript tries to infer types automatically from your code. When it sees this:

const result = []

there is no information about what kind of values the array should contain. In some contexts, TypeScript infers the type as never[].

never is a special TypeScript type that means this can never happen or no value is possible here. So a never[] means:

  • this is an array
  • but it can never contain any value

That is why this fails:

result.push(foo)

If result is inferred as never[], then push() expects values of type never. A string is not assignable to never, so TypeScript reports an error.

This is intended behavior, not a bug. TypeScript is being cautious because the empty array alone does not provide enough information about the element type.

To fix it, you need to tell TypeScript what the array should contain:

const result: string[] = []

or:

const result = [] as string[]

The first form is generally clearer and preferred.

Why this matters in real programming:

  • Type inference is helpful, but it needs clues.
  • Empty arrays often need explicit typing.
  • Understanding never helps you read confusing TypeScript errors more confidently.
  • This pattern appears often in reducers, accumulators, state initialization, and data transformation code.

Mental Model

Think of TypeScript as a very careful organizer labeling boxes.

If you write:

const result = []

you have given it an empty box with no label.

TypeScript asks:

  • Should this box hold strings?
  • numbers?
  • objects?
  • booleans?

Since it cannot safely guess, it may label the box as:

  • a box that should never receive anything

That is what never[] means.

Then when you try to put a string into the box:

result.push(foo)

TypeScript says: "You told me this box accepts nothing, so I cannot allow a string either."

The solution is to label the box up front:

const result: string[] = []

Now TypeScript knows this box is for strings.

Take Quiz

Syntax and Examples

Core idea

When creating an empty array in TypeScript, the element type may need to be provided explicitly.

Problem example

const foo = (foo: string) => {
  const result = []
  result.push(foo)
}

TypeScript may infer:

const result: never[]

So this becomes invalid:

result.push(foo) // string is not assignable to never

Correct ways to write it

1. Explicit array type

const foo = (foo: string) => {
  const result: string[] = []
  result.push(foo)
}

This is the clearest option. It tells TypeScript that result is an array of strings.

2. Type assertion

const foo = (foo: string) => {
  const result = [] as string[]
  result.push(foo)
}

This also works, but explicit type annotations are usually easier to read.

3. Provide inference from initial values

const result = ["hello"]
result.push("world")

Here TypeScript can infer string[] because the array already contains a string.

Another example with numbers

const values: number[] = []
values.push(1)
values.push(2)

If you leave off the type in a context where TypeScript cannot infer it, you may get the same never[] issue.

Function return example

const makeList = (item: string): string[] => {
  const result: string[] = []
  result.push(item)
  return result
}

This is a common and correct pattern.

Step by Step Execution

Consider this code:

const addName = (name: string) => {
  const names = []
  names.push(name)
}

Here is what happens step by step:

  1. TypeScript reads the function parameter:

    name: string
    

    So name is known to be a string.

  2. TypeScript reads the empty array:

    const names = []
    

    There are no elements, so there is no direct clue about the array's element type.

  3. In this situation, TypeScript may infer:

    const names: never[]
    
  4. TypeScript then checks this line:

    names.push(name)
    
  5. If names is never[], then push() only accepts never values.

  6. But name is a string, not never.

  7. TypeScript reports:

    Argument of type 'string' is not assignable to parameter of type 'never'.
    

Now compare it with the fixed version:

const addName = (name: string) => {
  const names: string[] = []
  names.push(name)
}

Step by step here:

  1. name is a string.
  2. names is explicitly declared as string[].
  3. push() expects a string.
  4. name is a string.
  5. The code type-checks successfully.

Real World Use Cases

This issue commonly appears in real code whenever you start with an empty array and fill it later.

1. Building a filtered result

const getShortNames = (names: string[]) => {
  const result: string[] = []

  for (const name of names) {
    if (name.length < 5) {
      result.push(name)
    }
  }

  return result
}

2. Collecting validation errors

const validateUser = (name: string, age: number) => {
  const errors: string[] = []

  if (!name) errors.push("Name is required")
  if (age < 18) errors.push("Must be at least 18")

  return errors
}

3. Transforming API data

type User = { id: number; name: string }

const extractNames = (users: User[]) => {
  const names: string[] = []

  for (const user of users) {
    names.push(user.name)
  }

  return names
}

4. Building log messages

const buildLogs = (items: number[]) => {
  const logs: string[] = []

  for (const item of items) {
    logs.push(`Processed ${item}`)
  }

  return logs
}

In all of these cases, the array starts empty, so giving it an explicit type is often the safest choice.

Take Quiz

Real Codebase Usage

In real projects, developers usually avoid this problem in a few common ways.

Explicitly type accumulators

This is very common in loops, reducers, and helper functions.

const ids: number[] = []
const messages: string[] = []

Use typed function return values

A return type can make the intent clearer.

const collectTags = (input: string[]): string[] => {
  const tags: string[] = []

  for (const item of input) {
    tags.push(item.trim())
  }

  return tags
}

Use array methods that preserve type information

Methods like map and filter often avoid manual empty-array setup.

const names = users.map(user => user.name)

This is often simpler than manually pushing into an array.

Use guard clauses before adding values

const collectValidNames = (values: Array<string | undefined>) => {
  const result: string[] = []

  for (const value of values) {
    if (!value) continue
    result.push(value)
  }

  return result
}

Type object properties that start as empty arrays

type State = {
  errors: string[]
}

const state: State = {
  errors: []
}

This gives the empty array context, so TypeScript knows what it should hold.

In reducers, type the accumulator

const words = ["a", "bb", "ccc"]

const lengths = words.reduce<number[]>((acc, word) => {
  acc.push(word.length)
  return acc
}, [])

Without the generic type argument, the empty array initializer may not have enough context.

Common Mistakes

1. Assuming TypeScript will always infer the array element type

Broken example:

const result = []
result.push("hello")

Why it fails:

  • The empty array gives no clear element type.
  • TypeScript may infer never[].

Fix:

const result: string[] = []
result.push("hello")

2. Using any[] just to silence the error

const result: any[] = []
result.push("hello")
result.push(123)
result.push(true)

Why this is risky:

  • It removes useful type safety.
  • Bugs become easier to introduce.

Better:

const result: string[] = []

3. Confusing never with unknown

  • never means no value is possible.
  • unknown means a value exists, but its type is not known yet.

These are very different.

4. Using type assertions too broadly

const result = [] as string[]

This works, but if overused, assertions can hide mistakes. Prefer annotations when possible:

const result: string[] = []

5. Forgetting to type nested empty arrays

Broken example:

type Group = {
  names: string[]
}

const group = {
  names: []
}

group.names.push("Sam")

Depending on the context, names may not get the type you expect.

Better:

const group: Group = {
  names: []
}

6. Ignoring context differences

Sometimes TypeScript can infer correctly if there is enough context:

const names: string[] = []

or:

function buildNames(): string[] {
  const names = []
  names.push("A")
  return names
}

But relying on inference in tricky cases can be confusing. Being explicit is often clearer.

Comparisons

never[] vs other array typings

TypeMeaningCan you push a string?Typical use
never[]Array that should contain no possible valuesNoUsually appears from inference when TypeScript has no safe element type
string[]Array of stringsYesNames, messages, labels
number[]Array of numbersNoScores, IDs, counts
unknown[]Array of unknown valuesYes, but reading values requires narrowingGeneric external data
any[]Array of anythingYesUsually avoided unless necessary

never vs unknown vs any

TypeMeaningSafe?Notes
neverNo value can existYes, very strictOften used in unreachable code or failed inference cases
unknownA value exists, but its type is not known yetYesYou must narrow before using it
anySkip type checkingNoConvenient, but removes protection

Explicit type annotation vs assertion

ApproachExampleRecommended?Reason
Type annotationconst result: string[] = []YesClear and safe
Type assertionconst result = [] as string[]SometimesWorks, but easier to misuse

Manual push vs map

PatternExampleWhen useful
Manual pushBuild an array step by stepGood for conditional logic or loops
mapTransform each item into a new arrayGood when each input becomes one output

Example:

const names = users.map(user => user.name)

This often avoids empty-array typing issues completely.

Take Quiz

Cheat Sheet

Quick fix

If you see:

Argument of type 'X' is not assignable to parameter of type 'never'

and the code includes an empty array like this:

const result = []

try typing the array explicitly:

const result: string[] = []

Key idea

  • Empty arrays may not provide enough type information.
  • TypeScript may infer never[].
  • never[] accepts no values.

Common fixes

const result: string[] = []
const values: number[] = []
const items: MyType[] = []

Alternative fix

const result = [] as string[]

Prefer this

const result: string[] = []

Meaning of never

  • never = impossible value
  • never[] = array that cannot contain any element

Watch for these places

  • accumulators
  • reducers
  • object properties initialized with []
  • state initialization
  • helper functions building arrays in loops

Better inference with context

const result: string[] = []

or

const result = ["a"]

Avoid unless necessary

const result: any[] = []

This removes type safety.

FAQ

Why does TypeScript infer never[] for an empty array?

Because an empty array alone does not tell TypeScript what element type it should contain. In some contexts, the safest inference is never[].

Is not assignable to parameter of type never a TypeScript bug?

No. It is intended behavior based on TypeScript's type inference rules.

How do I fix parameter of type never when using .push()?

Give the array an explicit type, such as:

const result: string[] = []

Should I use as string[] or : string[]?

Usually : string[] is better because it is clearer and more explicit.

What does never mean in TypeScript?

never means no value can exist for that type. It is often used for impossible code paths or failed inference scenarios.

Why does this happen more often with empty arrays than non-empty arrays?

A non-empty array gives TypeScript sample values to inspect. For example, ["a"] clearly suggests string[], but [] gives no clue.

Can this happen with objects too?

Yes. Empty arrays inside objects can also need context or explicit typing.

Is using any[] a good fix?

Usually no. It hides the error but removes type safety. Prefer a specific array type instead.

Take Quiz

Mini Project

Description

Build a small TypeScript utility that collects valid usernames from an input array. This demonstrates how to correctly type an initially empty array and safely push values into it after validation.

Goal

Create a function that returns a string[] of valid usernames without triggering never[] errors.

Requirements

  • Write a function that accepts an array of strings or undefined values.
  • Create an empty result array with the correct TypeScript type.
  • Skip undefined values and blank strings.
  • Add trimmed valid usernames to the result.
  • Return the final array of usernames.
Take Quiz

Keep learning

Related questions

View more