In TypeScript, why does `interface Settings { [key: string]: string; retries: number }` fail to compile, and what are your options for fixing it?
answer
- one universal claim over all keys
- declared property is also a key
- assignability runs member → signature
- widen the value type or split the shape
- error names the 'string index type'
basics
~20 sThe index signature promises that every string key holds a string, and retries is a string key holding a number, so TypeScript rejects it. Fix it by widening the signature's value type, moving the open bag into its own property, or dropping the signature.
solid answer
~50 sAn index signature is a claim about **every** key of that kind, and a declared property is one of those keys — so `retries` must be assignable to the signature's value type. Here it is a `number` against `string`, and the compiler reports that the property's type is not assignable to the `string` index type. There are three honest fixes. Widen the signature to `[key: string]: string | number`, which keeps one flat type but makes every arbitrary read a `string | number` you have to narrow. Split the shape — `{ retries: number; labels: { [key: string]: string } }` — so the known fields keep exact types and only the open part is a dictionary; this is usually the right answer. Or drop the index signature entirely if the keys were actually known. The same check applies to every declared member, methods included.
code
typescript · 15 lines// Fix 1: widen the signature's value type
interface Widened {
[key: string]: string | number;
retries: number; // declared property keeps its exact type
}
// Fix 2: keep the open bag in its own property
interface Split {
retries: number;
labels: { [key: string]: string };
}
const a: Widened = { retries: 3, region: "eu" };
const b: Split = { retries: 3, labels: { region: "eu" } };
console.log(a.retries, b.labels["region"]);go deeper
Recall that a declared property must fit the index signature's value type, and be able to point at the offending member when the compiler complains.
Explain why the rule exists — the checker needs one answer for a read with a computed key — and walk through widening versus splitting the type.
Choose the fix on modelling grounds: confine the open bag to its own property so known fields keep precise types, and refuse widened unions that spread narrowing work across callers.
Own the convention for boundary shapes carrying arbitrary extras, including whether the intersection workaround is ever allowed and what run-time validation must sit behind it.
## The rule in one sentence When an object type has an index signature, **every declared property must be assignable to that signature's value type**. The signature is not a fallback for keys you forgot to list; it is a universal claim covering all keys of that kind, and the declared properties are a subset of those keys. ## Why the rule exists The checker must be able to answer `obj[someString]` with a single type. If `retries` were allowed to hold a `number` while the signature said `string`, then this would type-check and lie: ```ts declare const k: string; const v: string = settings[k]; // k could be "retries" at run time ``` There is no way for the compiler to know that `k` is not `"retries"`, so the only way to keep index reads meaningful is to require that the declared properties already fit. Without the rule, the index signature would be worthless: any read through it could be any of the declared types. ## The error you will see The compiler is specific: *Property 'retries' of type 'number' is not assignable to 'string' index type 'string'.* Note what it is **not** saying. It is not saying you may not mix declared properties with an index signature — that is the most common misreading. Mixing is fine and normal: ```ts interface Headers { [name: string]: string; contentType: string; // fine accept: string; // fine } ``` Only incompatible members are rejected. And because a method is just a property whose type is a function type, a method declaration is checked the same way — a method on an interface with `[key: string]: string` fails for exactly the same reason. ## Fix 1 — widen the value type ```ts interface Settings { [key: string]: string | number; retries: number; } ``` This compiles, and `settings.retries` is still exactly `number` because a declared property keeps its own, more specific type. The cost lands on every *other* read: `settings[someKey]` is now `string | number`, so callers must narrow before using it. Widen once and the union tends to grow — `string | number | boolean | undefined` is the smell that this fix has been applied too many times. ## Fix 2 — split the known from the open (usually right) ```ts interface Settings { retries: number; timeoutMs: number; labels: { [key: string]: string }; } ``` The fields you know keep exact types, autocomplete and typo checking. The genuinely open part is confined to one property, and only reads of `labels` need the dictionary discipline. This models reality better: most "config plus arbitrary extras" shapes really are two different things glued together. ## Fix 3 — delete the index signature Often the signature was added to silence a different complaint — to make an object accept a computed key, or to let a value pass where a wider type was expected. If the key set is actually known, remove the signature and list the properties. You get back everything the signature was suppressing. ## The intersection escape hatch, and why it is a hole The conformance check is performed per object-type declaration, so an intersection sidesteps it: ```ts type Settings = { retries: number } & { [key: string]: string }; ``` This compiles. It also produces a type that claims something false — an arbitrary string read yields `string`, yet `retries` holds a number — so a lookup with a computed key can hand you a number typed as `string`. Recognise the trick, and treat it as a deliberate unsoundness rather than a fix; if you use it, keep it behind a boundary that validates at run time. ## What an interviewer is listening for The weak answer is "you cannot mix them". The strong answer names the direction of the constraint (declared members must fit the signature, not the other way round), explains it from the need to give `obj[k]` a single answer, and then chooses between widening and splitting on modelling grounds rather than on which one makes the red squiggle go away.
- If you widen the signature to `string | number`, what type does `settings.retries` have?Still exactly `number`. A declared property keeps its own type; the index signature only supplies a type for keys that are not declared. That asymmetry is why widening is bearable — known access stays precise, and only reads with a computed key pay the union tax.
- Does the same conformance check apply to methods declared on the interface?Yes. A method is a property whose type is a function type, so it must be assignable to the index signature's value type like any other member. On an interface with `[key: string]: string`, any method declaration fails. That is a strong hint that the shape is a model, not a dictionary.
saying these in an interview costs you the question
- Says an interface cannot mix declared properties with an index signature
- Gets the direction backwards — expects the signature to adapt to members
- Widens the value type to any to make the error go away
- Thinks marking the property optional satisfies the check
- Treats the intersection workaround as a real fix rather than a hole