skip to content

Object Shape Features

The per-member vocabulary you use inside an object type: optionality, immutability, open-ended keys, and the extra checks the compiler runs on object literals. These features are where day-to-day modeling bugs live, so interviewers probe them with small, concrete snippets.

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

questions

14

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 a TypeScript interface, what does the `?` in `email?: string` change — for code that builds the object, for code that reads the property, and for the emitted JavaScript?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Marking a property with ? makes it optional: an object may omit the key, and reading it yields string | undefined under strictNullChecks. The modifier is compile-time only and changes nothing in the emitted JavaScript.

open as a page

Given `interface Point { x: number; y: number }` in TypeScript, why does `const p: Point = { x: 1, y: 2, z: 3 }` fail with "Object literal may only specify known properties", while `const tmp = { x: 1, y: 2, z: 3 }; const p: Point = tmp;` compiles cleanly?

level: middleimportance: must knowfreq 74%

basics

~20 s

TypeScript applies an extra excess-property check to fresh object literals assigned straight to a typed target. Storing the literal in a variable first widens it and drops that freshness, leaving only ordinary structural assignability, which happily permits extra properties.

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 a TypeScript object type, what is the difference between declaring `email?: string` and declaring `email: string | undefined`?

level: middleimportance: must knowfreq 66%

basics

~20 s

The optional form lets the key be omitted; the union form requires the key to be present, even if its value is undefined. Both read as string | undefined, and under default settings the optional form also accepts an explicit undefined.

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

In TypeScript, an object carrying a property that its declared type does not list is passed to a function. Does the emitted JavaScript still contain that property at run time, and what does that mean if the function serialises the object?

level: juniorimportance: should knowfreq 44%

basics

~20 s

Yes, the property is still there. TypeScript erases types and never rewrites values, so nothing strips undeclared keys. The excess-property check is a compile-time typo guard only; serialising the object sends the extra key over the wire.

open as a page

Given `interface Options { retries?: number; timeout?: number }` in TypeScript, why does `const o = { retres: 3 }; const opts: Options = o;` still error, even though `o` is a variable rather than a fresh object literal?

level: middleimportance: should knowfreq 38%

basics

~20 s

Options is a weak type — every property is optional — so almost anything is structurally assignable to it. TypeScript adds a weak-type check: a source with properties but none in common with the target is rejected, and it applies to variables too.

open as a page

In TypeScript, what does the `readonly` modifier on an object-type property such as `readonly db: { url: string }` actually prevent, and what does it leave untouched?

level: middleimportance: should knowfreq 55%

basics

~20 s

readonly stops the checker from accepting an assignment to that property through that type. It is shallow, so the object the property points at stays fully mutable, and it is erased at compile time, so nothing is protected at runtime.

open as a page

In TypeScript, a teammate silences an "Object literal may only specify known properties" error by writing `as Config` on the literal. What does that assertion give up, and what should they use instead — including where `satisfies Config` does and does not help?

level: seniorimportance: should knowfreq 46%

basics

~20 s

as removes the literal's freshness, so it silences every excess-property complaint in that literal, not just the intended one — future typos included. The honest fixes change the type: declare the extra property, or widen the contract. satisfies checks without widening but still rejects undeclared keys.

open as a page

What does enabling TypeScript's `exactOptionalPropertyTypes` compiler option change about optional properties, and how do you declare one that must still accept an explicit `undefined`?

level: seniorimportance: should knowfreq 36%

basics

~20 s

With exactOptionalPropertyTypes on, a property marked ? means only "the key may be missing" — writing the key with the value undefined becomes an error. To allow that, declare the type explicitly as name?: string | undefined.

open as a page

A TypeScript object whose property is declared `readonly` still gets overwritten at runtime, with no compile error anywhere in the codebase. What holes in the modifier allow that, and what would actually stop it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Assignability ignores readonly on object properties, so the value can be assigned to an alias typed without it and written through that alias. The modifier is also shallow and erased, so nested data, assertions, and untyped callers bypass it entirely.

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