skip to content

Index Signatures and Dictionary Types

How to type an object with unknown keys, and the constraint that every declared property must be compatible with the index signature. Interviewers ask when to model a dictionary versus a fixed record, and whether you know that indexing is unsound by default unless noUncheckedIndexedAccess is on.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

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?

level: juniorimportance: must knowfreq 65%

answer

  1. keys are data, not a fixed list
  2. square brackets inside the type body
  3. [key: string]: T
  4. the bracket name is documentation only
  5. no key is promised to exist

basics

~10 s

Declare 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 s

You 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 lines
typescript
interface 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 time

go deeper

for a junior

Be able to write [key: string]: T from memory, say what it allows, and state plainly that no specific key is guaranteed to exist.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

In TypeScript, why does `interface Settings { [key: string]: string; retries: number }` fail to compile, and what are your options for fixing it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The 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.

open as a page

In TypeScript, reading a key that is absent from a dictionary type such as `{ [id: string]: User }` still type-checks and yields `User` rather than `User | undefined`. Why does the checker behave that way, and how do you get honest types for those lookups?

level: seniorimportance: must knowfreq 50%

basics

~20 s

By default an index-signature read is typed as the value type, because the checker cannot know which keys exist and assuming presence keeps everyday code ergonomic. Enable noUncheckedIndexedAccess to make such reads yield the value type plus undefined.

open as a page

A service returns a bag of feature flags keyed by flag name. In TypeScript, argue the tradeoffs between typing it as an open dictionary with a string index signature and typing it as a closed object type with one property per known flag.

level: principalimportance: should knowfreq 30%

basics

~20 s

An open dictionary never goes stale but gives up autocomplete, typo detection and any presence guarantee; a closed object type restores all three but becomes a false claim the moment the service ships a flag it does not list. The usual answer is both, separated by a validating boundary.

open as a page

In TypeScript, why is `interface Lookup { [i: number]: string | number; [key: string]: string }` rejected, while swapping the two value types compiles?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

When 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.

open as a page