In TypeScript, how do you type an object whose property names are not known in advance — say a lookup from user id to user name — and what does that type then allow and forbid?
answer
- keys are data, not a fixed list
- square brackets inside the type body
- [key: string]: T
- the bracket name is documentation only
- no key is promised to exist
basics
~10 sDeclare an index signature: interface UserNames { [id: string]: string }. Any string key may then be read or written and is typed string, but the type guarantees that no particular key actually exists.
solid answer
~40 sYou give the object type an **index signature** — `interface UserNames { [id: string]: string }`. The identifier in the brackets is documentation only; what the compiler uses is the key type (`string`, `number`, `symbol`, or a template-literal pattern) and the value type. Any key of that kind can then be read or written, and every read is typed as the value type, so `names["anything"]` is a `string` and assigning a `number` to it is an error. What you give up is per-key knowledge: there is no autocomplete for keys, a typo is not an error, and by default reading a key that was never set still type-checks. Reach for it when the keys genuinely come from data; use a closed object type listing each property when you already know them.
code
typescript · 10 linesinterface UserNames {
[id: string]: string;
}
const names: UserNames = {};
names["u_1"] = "Ada";
names.u_2 = "Grace"; // dotted access is allowed by default
const first: string = names["u_1"];
console.log(first, names["nobody"]); // compiles; undefined at run timego deeper
Be able to write [key: string]: T from memory, say what it allows, and state plainly that no specific key is guaranteed to exist.
Explain that the bracket name is only a label, which key types are legal, and why every declared property must fit the signature's value type.
Show the production judgment: dictionary reads are the main source of stray undefined, so argue for bracket access, precise value types, and honest lookup types across the codebase.
Own the modelling policy — where in the system an open key set is legitimate, where it must be parsed into a closed type, and what that boundary costs the team.
## The problem an index signature solves An object type normally works by listing property names: `interface User { id: string; name: string }` says exactly which properties exist and what each holds. That breaks down as soon as the *keys themselves are data* — user ids, locale codes, feature names, header names. You cannot enumerate them at compile time, and you do not want to. An index signature declares a **rule for keys** instead of a list of them: ```ts interface UserNames { [id: string]: string; } ``` Read that as: "every string key of this object holds a string". It is one declaration that covers infinitely many possible properties. ## Anatomy of the declaration The syntax is `[<name>: <keyType>]: <valueType>`. - `<name>` is **arbitrary**. `[id: string]`, `[key: string]` and `[k: string]` are the same type. It exists purely so a reader knows what the key means; nothing checks it. - `<keyType>` is restricted. It may be `string`, `number`, `symbol`, or a template-literal pattern such as `` `data-${string}` `` — not an arbitrary type. - `<valueType>` is the type every matching property is assumed to hold. Once declared, the checker treats element access as returning the value type: ```ts const names: UserNames = {}; names["u_1"] = "Ada"; // ok: string value names["u_2"] = 42; // error: number is not assignable to string const n: string = names["u_1"]; ``` Dotted access works too by default (`names.u_1`), which surprises people who expect brackets to be mandatory. The compiler flag `noPropertyAccessFromIndexSignature` turns that off, so dotted access is reserved for properties you actually declared and bracket access marks "this key came from data". ## This is a compile-time rule and nothing else TypeScript erases types on emit, so an index signature produces **no runtime code at all**. It does not validate keys, it does not restrict what can be written by untyped JavaScript, and it does not make the object a `Map`. It is a claim you are making to the checker about the shape of the data, and the checker takes you at your word. Everything an index signature buys you disappears the moment a value arrives from `JSON.parse`, an untyped library, or an `as` assertion. ## What you trade away The cost is precision, and it is real: - **No autocomplete.** The editor cannot suggest keys, because the type does not know any. - **No typo detection.** `names["nmae"]` is a perfectly good `string` key, so it type-checks. - **No knowledge of presence.** By default a read of an absent key is still typed as the value type rather than `valueType | undefined`, which is the single biggest source of runtime `undefined` in dictionary-heavy code. Turning on `noUncheckedIndexedAccess` changes reads to include `undefined`. An index signature is therefore not a "looser interface" you reach for when a shape is inconvenient — it is a deliberate statement that the key set is open. ## Mixing declared properties in You may declare named properties alongside an index signature, but each one must be compatible with it, because a named property is also a key that the signature claims to cover: ```ts interface Headers { [name: string]: string; contentType: string; // fine: string } ``` Adding `retries: number` to that same interface is rejected — the signature promised every string key holds a `string`. ## Dictionary or model? The practical decision is whether the key set is **open** (comes from data, grows without a code change) or **closed** (you know it, and adding one is a code change). Open key sets want an index signature; closed key sets want a plain object type with one property each, so you get autocomplete, typo errors and exhaustive handling. Writing an index signature for a shape you actually know is the common junior mistake, and it silently disables every check that would have caught a bug. One related shorthand worth recognising: `Record<string, string>` is the built-in alias that produces the same open dictionary shape without writing the bracket syntax. ## Rules of thumb Use an index signature when keys are data; type the value as precisely as you can (avoid `any`); prefer bracket access for index-signature keys so the read is visibly untrusted; and turn on `noUncheckedIndexedAccess` early, before the codebase fills up with reads that assume presence.
- Does the identifier you write inside the brackets, like `id` in `[id: string]`, affect type checking at all?No. It is a documentation label for readers; `[id: string]: T`, `[key: string]: T` and `[k: string]: T` are identical types. Only the key type and the value type are checked. Choosing a meaningful name is still worth doing, because it tells the next reader what the keys mean — user ids, locale codes, header names — which the type itself cannot express.
- Which key types is an index signature parameter allowed to use?`string`, `number`, `symbol`, or a template-literal pattern such as `` `data-${string}` `` — not arbitrary types. A pattern signature is useful for prefix conventions: it constrains keys to those matching the pattern while leaving the rest of the object's declared properties alone.
- How is a dictionary typed with an index signature different from a `Map`?An index signature describes a plain object at the type level only and vanishes on compile; a `Map` is a real runtime data structure with its own API, non-string keys, a `size`, and insertion-ordered iteration. If you need non-string keys, frequent deletion, or a real presence check, `Map` is the better structure — the index signature only ever describes an object you were going to have anyway.
An index signature is a policy, not a roster: it says what any pigeonhole of the right shape must contain, without claiming which pigeonholes have been built.
saying these in an interview costs you the question
- Thinks an index signature validates keys at run time
- Believes the name in the brackets must match real property names
- Uses [key: string]: any to avoid modelling the value type
- Reaches for an index signature when the keys are actually known
- Assumes reading a key that was never set is a compile error