In TypeScript, why is `interface Lookup { [i: number]: string | number; [key: string]: string }` rejected, while swapping the two value types compiles?
answer
- two signatures are not independent
- string signature covers every key
- assignability runs numeric into string
- object keys are strings underneath
- numeric one must be the narrower
basics
~20 sWhen a type declares both index signatures, the numeric signature's value type must be assignable to the string signature's, because a numeric key is reachable as a string key too. Here string | number does not fit string, so it is rejected.
solid answer
~40 sA type may declare a numeric and a string index signature at once, but they are not independent: the string signature covers *all* keys, so anything the numeric signature describes must also satisfy it. TypeScript enforces that with a dedicated check — the numeric index type must be assignable to the string index type. In the rejected version the numeric values are `string | number` while the string signature promises `string`, so a lookup with a computed string key could hand back a number typed as `string`. Swapping them — numeric `string`, string `string | number` — compiles, because the narrower type sits on the numeric side. The rule mirrors the fact that object property keys are strings underneath, which is the JavaScript behaviour the checker is modelling.
code
typescript · 9 linesinterface Ok {
[i: number]: string; // narrower
[key: string]: string | number; // wider: must cover the numeric case
}
declare const row: Ok;
const byIndex: string = row[0]; // numeric signature wins
const byName: string | number = row["tag"]; // string signature
console.log(byIndex, byName);go deeper
Know that both a numeric and a string index signature can appear on one type, and that the compiler constrains how their value types relate.
State the direction of the constraint and justify it: the string signature covers every key, so the numeric value type must be assignable to it.
Recognise the opaque error in real code, and steer positional data toward array or tuple types rather than hand-written numeric signatures.
Use this as an example of the type layer modelling a runtime fact, and set the convention for how array-like external shapes are described in shared declarations.
## Two signatures, one object An object type may declare both forms: ```ts interface Row { [i: number]: string; [key: string]: string; } ``` The numeric signature is there for **array-like** shapes — things indexed positionally, where you want `row[0]` to mean something specific. The string signature is the general dictionary form. ## The constraint between them The two are not independent declarations sitting side by side. The string signature is a claim about *every* key, and a numerically-named key is one of them. So TypeScript requires: **the numeric index signature's value type must be assignable to the string index signature's value type.** Violating it produces a message of the form *Numeric index type '…' is not assignable to string index type '…'*. Applied to the question: - `[i: number]: string | number` with `[key: string]: string` — rejected. The string signature says every key yields a `string`, yet the numeric one admits numbers. - `[i: number]: string` with `[key: string]: string | number` — accepted. The narrower type is on the numeric side, so nothing the numeric signature promises contradicts the string signature. The direction is easy to remember: **numeric must fit inside string**, never the reverse. ## Where the rule comes from This is one of the places where the type layer is shaped by the runtime it sits on. In JavaScript, a plain object's property keys are strings (or symbols); writing `obj[0]` reaches the same property as `obj["0"]`. That is the JavaScript tree's territory, and the type-level consequence is what matters here: since a numerically-keyed property *is* a string-keyed property underneath, the string signature must be able to describe it. If the two signatures were allowed to disagree, a lookup with a computed string key could return a value of a type the string signature had ruled out — a hole with no way for the checker to close it. ## Which signature applies to a read When both are present, the checker picks the more specific one for the access it can see: ```ts declare const row: Row; row[0]; // uses the numeric signature row["label"]; // uses the string signature ``` So the numeric signature is not decorative when the string one is wider — it is how you say "positional access is more precise than arbitrary access", which is exactly the shape built-in array types have. ## The same conformance idea, applied twice There are really two conformance rules working together on an object type with index signatures: 1. Every declared property must be assignable to the signature that covers its key — a numerically-named member such as `0: string` is checked against the numeric signature. 2. The numeric signature must be assignable to the string signature. Both exist for the same reason: every read the checker can express must have one honest answer. ## Is a numeric index signature worth writing? In day-to-day application code, rarely. Positional data is better served by an array or a tuple type, which already carry a numeric index behaviour plus length and iteration. A numeric index signature earns its place when you are describing an existing array-like object you did not create — an arguments-style structure, a DOM-ish collection, or a legacy shape indexed by position but not an array. The reason to know the rule is that when you *do* hit it, the error message is opaque unless you already understand that the string signature outranks the numeric one. ## Interview framing This question is a probe for whether you see the type layer as a model of a runtime rather than a set of arbitrary rules. The satisfying answer is short: object keys are strings underneath, so the string signature has to cover the numeric one, and assignability therefore runs numeric → string.
- If both signatures are declared, which one types the expression `row[0]`?The numeric one. The checker uses the most specific signature it can apply to the access it sees, so a numeric key resolves through the numeric signature and an arbitrary string key falls to the string signature. That is what makes the pairing useful: positional reads stay precise while arbitrary reads stay permissive.
- When would you actually reach for a numeric index signature rather than an array type?Almost only when describing an array-like object you did not create — a legacy positional structure, or a host collection that is indexed but not a real array. An array or tuple type already gives numeric access plus `length`, iteration and the full array API, so for data you own it is the better model.
saying these in an interview costs you the question
- Thinks the two index signatures are checked independently
- Gets the direction backwards — expects string to fit numeric
- Claims a type may declare only one index signature
- Reaches for a numeric index signature where an array type belongs