In TypeScript, `Record<string, User>` and `{ [key: string]: User }` describe the same dictionary — so what do the two spellings actually differ in, and when would you reach for `Record`?
answer
- same type, two spellings
- one of them takes a union key
- one of them sits beside named members
- composition versus declaration
- interfaces lack an implicit index signature
basics
~20 sFor a string key they produce the identical type — Record<string, User> expands to that index signature. They differ as tools: Record is a generic alias whose key set can be a union, enum or computed type and which composes with other utilities, while an index signature is a declaration form that can sit inside an interface next to named members.
solid answer
~50 sThey are not two different types — `Record<string, User>` *is* `{ [key: string]: User }` after expansion, because the mapped type `{ [P in string]: User }` collapses to a string index signature. The difference is expressive range. `Record` is a generic alias you can use anywhere a type goes: the key set can be a union of literals, an enum, `keyof T`, or a template-literal type, which buys you exhaustiveness that an open index signature can never give. It also composes — `Partial<Record<Status, Handler>>`, `Readonly<Record<string, Config>>` — and reads well inline in a signature. The index-signature spelling earns its place when you are writing a declaration that also carries named members, or an interface that other code extends or augments. My default: `Record` when the key set is interesting or the type is used inline, an interface with an index signature when I am declaring a named shape with other members on it.
code
typescript · 18 linestype User = { id: string };
// Same type, two spellings
type A = Record<string, User>;
type B = { [key: string]: User };
declare const a: A;
const b: B = a;
// Only Record can take a finite key set
type Status = 'draft' | 'live';
type Labels = Record<Status, string>;
const labels: Labels = { draft: 'Draft', live: 'Live' };
// Only the declaration form sits beside named members
interface ApiResponse {
status: number;
[header: string]: unknown;
}go deeper
Be able to say that Record<string, T> and a string index signature describe the same dictionary, and that Record is the one that can also take a union of literal keys.
Explain that Record expands to the index signature, then name the real differences: a computable or finite key set, composition with Partial and Readonly, inline use, versus a declaration that can carry named members alongside the catch-all.
Diagnose the interface-versus-alias assignability failure against Record<string, unknown> and explain the declaration-merging reason behind it, rather than reaching for a cast.
Set the house convention: which dictionary shapes are public interfaces others extend and which are internal aliases, so the codebase does not accumulate both spellings of the same concept with different extension stories.
## They are the same type when the key is `string` Start with the fact that settles most of the argument. `Record` is defined as `{ [P in K]: T }`, and mapping over `string` produces a string index signature: ```ts type A = Record<string, User>; // { [x: string]: User } type B = { [key: string]: User }; declare const a: A; const b: B = a; // assignable both ways — identical structure ``` So "which is faster", "which is safer", or "which one checks the keys" are all the wrong questions for this pair: there is one type with two spellings. Neither validates anything at runtime, and both are erased. ## Where they genuinely diverge The divergence is about what each **form** can express, not about the resulting type. **Record can take a key set that is not open.** This is the big one. An index signature's key must be `string`, `number`, `symbol`, or a template-literal pattern — it cannot be an arbitrary union of literals, and it cannot be an enum: ```ts type Status = 'draft' | 'live'; type Labels = Record<Status, string>; // { draft: string; live: string } // there is no index-signature spelling of that ``` That is the case where the two tools stop being interchangeable, and it is usually the reason to pick `Record` in the first place: a finite key set gives you required entries and a build break when the union grows. **Record composes.** Because it is an ordinary type expression, it nests inside other utilities and appears inline: ```ts type Sparse = Partial<Record<Status, Handler>>; type Frozen = Readonly<Record<string, Config>>; function merge(base: Record<string, unknown>, patch: Record<string, unknown>) { /* … */ } ``` Writing those with index signatures means declaring a named type first, or repeating the signature at each site. **Record is generic, so the key set can be computed.** `Record<keyof Config, Validator>` forces one validator per field of `Config` and updates itself when `Config` changes. An index signature has no such handle on another type. **The index-signature form can coexist with named members.** This is where the other spelling wins: ```ts interface ApiResponse { status: number; [header: string]: unknown; // named members must be compatible with the index type } ``` A bare `Record` cannot express "these known fields plus a catch-all"; you would need an intersection, which reads worse and behaves less predictably at the excess-property check. Note the rule that comes with it: every declared property's type must be assignable to the index signature's value type, which is why the catch-all above is `unknown` rather than `string`. **Interfaces can be extended and augmented.** If the dictionary type is a public surface others build on, declaring it as an interface with an index signature keeps `extends` and declaration merging available. A type alias built from `Record` supports intersection but not merging. ## The assignability gotcha worth knowing One real difference bites in practice: an **interface** does not get an implicit index signature, but an object **type alias** does. So a value typed by an interface is rejected where `Record<string, unknown>` is expected, while the structurally identical alias is accepted: ```ts interface IUser { id: string } type TUser = { id: string }; declare function log(data: Record<string, unknown>): void; declare const i: IUser; declare const t: TUser; log(t); // ok log(i); // error: Index signature for type 'string' is missing in type 'IUser' ``` The reason is that an interface can be reopened by declaration merging elsewhere, so the compiler will not assume its full member list is known. The usual fixes are to declare the model as a type alias, to widen the parameter to `object` or a generic, or to spread the value at the call site. Candidates hit this constantly when passing a domain model into a logging or serialisation helper typed with `Record<string, unknown>`, and being able to explain *why* it happens is what separates a memorised workaround from understanding. ## Choosing in practice Ask what the key set is. If it is a finite, meaningful set — statuses, roles, feature keys, fields of another type — use `Record` and take the exhaustiveness. If the key set is genuinely open, both spellings give you the same type, so pick by context: `Record<string, T>` inline in a signature or composed with another utility, an interface with an index signature when you are declaring a named shape that carries other members or that other code extends. And whichever you pick, remember that an open key set means the compiler will type a lookup as present even when the key is absent — that risk is identical for both spellings.
- Why does passing a value typed by an interface into a parameter of type `Record<string, unknown>` fail, when an identical type alias is accepted?Because interfaces can be reopened by declaration merging, TypeScript will not give one an implicit index signature — it cannot assume the member list is final. Object type aliases are closed, so they get one and are assignable. Fixes: declare the model as a type alias, widen the parameter, or spread the value at the call site.
- Can you express "a known field plus arbitrary extra keys" with `Record` alone?Not cleanly. That shape wants an index signature declared alongside the named member, as in `interface R { status: number; [k: string]: unknown }`. An intersection such as `{ status: number } & Record<string, unknown>` type-checks but interacts awkwardly with excess-property checking and reads worse. Also remember the declared property's type must be assignable to the index signature's value type.
- Is there any runtime or performance difference between the two spellings?None. Both are erased entirely — the emitted JavaScript is the same object either way. The only cost difference is compile-time work, and for a plain string key the alias expands to the very same index signature, so even that is a wash. Choose on expressiveness and readability, not on cost.
saying these in an interview costs you the question
- Claiming Record<string, T> is safer than the equivalent index signature
- Saying an index signature can be keyed by a union of literals
- Thinking Record adds a runtime key check
- Assuming an interface is assignable to Record<string, unknown>
- Believing one spelling emits more JavaScript than the other