In TypeScript, what types are inferred for `const mixed = [1, 'a']` and `const tags = ['a', 'b']`, and why is `tags` not inferred as `('a' | 'b')[]`?
answer
- union of the elements, then widen
- the slots can still be written to
- const freezes the binding, not the contents
- literal types would be a promise it cannot keep
- annotation or assertion overrides the default
basics
~20 smixed is inferred as (string | number)[] and tags as string[]. TypeScript takes the union of the element types and widens literal types, because a mutable array's slots can later be reassigned to any other string.
solid answer
~40 sFrom an array literal, TypeScript infers a mutable array type whose element type is the union of the elements' inferred types, so `[1, 'a']` gives `(string | number)[]`. It does not infer a tuple — position and length are not preserved from a plain literal. The reason `['a', 'b']` becomes `string[]` rather than `('a' | 'b')[]` is literal widening: in a mutable location the compiler assumes the value can change, and since `tags[0] = 'c'` must be legal for a `string[]`, keeping the narrow literal types would be a promise the type could not keep. Two things override this. A contextual type wins: annotating `const tags: ('a' | 'b')[]` keeps the literals. And a `const` assertion opts out of widening entirely, producing a readonly tuple of literal types.
code
typescript · 9 linesconst ids = [1, 2, 3]; // number[]
const mixed = [1, 'a']; // (string | number)[]
const tags = ['a', 'b']; // string[], not ('a' | 'b')[]
tags[0] = 'anything'; // legal — which is why the element type widened
const pinned: ('a' | 'b')[] = ['a', 'b']; // contextual type wins
// pinned[0] = 'c'; // Error: 'c' is not assignable to 'a' | 'b'
console.log(ids.length, mixed.length, tags[0], pinned[0]);go deeper
Be able to state the two results — (string | number)[] and string[] — and say that TypeScript unions the element types. Knowing that const does not make an array's contents immutable is the detail worth having ready.
Explain both mechanisms separately: union formation across elements, and literal widening in mutable positions. Say why widening is required for soundness, using an index assignment as the example that would otherwise break.
Show where inference is overridden in real code — contextual types from parameters and annotations, and the const-assertion escape hatch — and give an example where relying on default widening loses a distinction the API needed.
Own the API-design consequence: whether a shared signature should demand narrow literal element types or accept the widened form determines how much friction every caller inherits, and pushing narrowness into a public type is a decision with a real ergonomic cost.
## The rule in one line Given an array literal with no contextual type, TypeScript infers `E[]` where `E` is the union of the inferred types of the elements, after each element's literal type has been widened. ```typescript const ids = [1, 2, 3]; // number[] const mixed = [1, 'a']; // (string | number)[] const tags = ['a', 'b']; // string[] const flags = [true]; // boolean[] ``` Two separate mechanisms are at work — union formation and widening — and interviews probe both. ## Union of element types, not a tuple A plain array literal produces an array type, never a tuple type. Length and per-position types are discarded: `[1, 'a']` and `['a', 1]` both infer `(string | number)[]`. That is deliberate. Arrays are mutable and resizable, so a type that pinned index 0 to `number` would be falsified by the very first `push` or reassignment. If you need positional types, you ask for them explicitly — either with an annotation or a `const` assertion — rather than getting them from a bare literal. When the elements share a common supertype the union is still a union of the specific types, not the supertype: ```typescript class Dog { bark() {} } class Cat { meow() {} } const pets = [new Dog(), new Cat()]; // (Dog | Cat)[] ``` This matters because the union preserves what each element can actually do, at the cost of requiring a narrowing check before you call either method. ## Widening: why the literal types disappear A string literal like `'a'` has, momentarily, the literal type `'a'`. Whether that survives depends on where it lands. In a mutable location — a `let` binding, an object property, an array element — the compiler widens it to the base primitive type, because the location can be written to again later: ```typescript let single = 'a'; // string, not 'a' const single2 = 'a'; // 'a' — an immutable binding keeps the literal type const tags = ['a', 'b']; // string[] — the elements are mutable slots ``` Note the asymmetry in the third line: `tags` is a `const` binding, but `const` only freezes the binding, not the array's contents. `tags.push('c')` and `tags[0] = 'c'` are both legal JavaScript, so the element type must be wide enough to accept them. Widening the element type to `string` is what makes the array type honest about its own mutability. ## What overrides the default **A contextual type.** If the literal is checked against an expected type, that type drives inference instead: ```typescript const tags: ('a' | 'b')[] = ['a', 'b']; // element type stays narrow function take(xs: readonly string[]) {} take(['a', 'b']); // literal checked against readonly string[] ``` The same happens for arguments, return positions and object properties with declared types — the annotation is the authority, and inference only fills the gap when there is none. **A `const` assertion.** Writing `['a', 'b'] as const` suppresses widening and produces a readonly tuple of literal types instead of a mutable array — the mechanics of that assertion are a topic of their own, but it is the standard escape hatch when you want the narrow types. ## The empty literal `const xs = []` has no elements to union. Under `noImplicitAny`, rather than erroring immediately, TypeScript treats this specific pattern as an evolving array: the element type is built up from what you subsequently push into the variable, and the type at any use site reflects what has been added so far. ```typescript const xs = []; xs.push(1); xs.push('a'); xs; // (string | number)[] ``` This only applies to a variable initialised with an empty literal; an empty literal in an expression position with no contextual type has nothing to evolve from, which is why library helpers usually annotate it — `const xs: number[] = []`. ## Why interviewers ask this The question separates people who have memorised "TypeScript infers types" from people who know what it infers and why. The useful framing is that inference tries to produce the type that will still be true after the value is used the way its mutability allows. A mutable array with narrow literal elements would not survive that test, so the compiler widens. Once you can state that principle, the `const`-assertion and contextual-type exceptions fall out of it: both are ways of telling the compiler that the mutability assumption does not hold.
- Why does `const x = 'a'` keep the literal type `'a'` while `const tags = ['a']` widens to `string[]`?Because `const` makes the binding immutable, so nothing can ever write a different string to `x` — the literal type stays true. An array element is a mutable slot regardless of how the array was bound: `tags[0] = 'z'` is legal. Widening keeps the element type honest about what can be written there.
- What happens to inference when the array literal is passed directly as an argument?The parameter's declared type becomes the contextual type, so inference is checked against it rather than run freely. Passing `['a', 'b']` to a parameter typed `('a' | 'b')[]` keeps the literal types; passing it to `string[]` produces `string[]`. This is why the same literal can have different types in different call sites.
- Does an array literal ever infer as a tuple without an annotation or assertion?Not from the literal itself in an ordinary variable declaration — a bare literal infers an array type. Tuple types come from a contextual type that is a tuple, from a const assertion, or from a generic parameter deliberately set up to infer positionally. If you want positions preserved, you have to ask for them.
saying these in an interview costs you the question
- Says ['a','b'] infers as ('a' | 'b')[]
- Thinks array literals infer tuple types by default
- Believes const makes the array's contents immutable
- Claims [1,'a'] infers any[] because it is mixed
- Expects the widened type to change after a push