skip to content

In TypeScript, a value typed by `interface Config { url: string }` is rejected when passed to a parameter of type `Record<string, unknown>`, while the same value typed by `type Config = { url: string }` is accepted. Why does the choice of declaration form change that?

level: middleimportance: should knowfreq 38%

answer

  1. one shape compiles, the other does not
  2. the target needs an index signature
  3. who can add members later?
  4. interfaces are open declarations
  5. implicit index signature for object literal types

basics

~20 s

Object-literal type aliases receive an implicit index signature; interfaces do not, because an interface can gain members from a later declaration, so the compiler will not treat its member list as final. Add an explicit index signature or use an alias.

solid answer

~50 s

Assigning to `Record<string, unknown>` requires the source type to have an index signature, and TypeScript hands an *implicit* one to object literal types — including a type alias for an object literal — because it knows every member of them. It withholds it from interfaces, because an interface is open: another declaration elsewhere in the program can add members to it, so the compiler cannot promise that every property is compatible with the index signature's value type. The error says an index signature for type `string` is missing in `Config`. Practical fixes, in order of preference: declare the shape as a type alias if nothing needs to extend it; add an explicit `[key: string]: unknown` member to the interface; or spread at the call site with `{ ...config }`, since the fresh object literal type gets the implicit signature.

code

typescript · 13 lines
typescript
interface Config { url: string }
type ConfigAlias = { url: string };

declare function send(payload: Record<string, unknown>): void;

const a: Config = { url: "/api" };
const b: ConfigAlias = { url: "/api" };

// @ts-expect-error index signature for type 'string' is missing in type 'Config'
send(a);

send(b);        // ok: object literal alias gets an implicit index signature
send({ ...a }); // ok: a fresh object literal type gets one too

go deeper

for a junior

Recognise the phrase "index signature is missing" as a mismatch about extra keys rather than about the property you declared, and know that rewriting the declaration as type often makes it go away.

for a middle

Explain the mechanism: object literal types and their aliases get an implicit index signature because their members are fully known, while an interface may be extended by a later declaration, so the compiler withholds it.

for a senior

Diagnose it at a real boundary and pick the right repair for the situation — alias, explicit index signature, spread, or a better parameter type — and argue against an assertion, which suppresses the check instead of expressing intent.

for a principal

Treat this as evidence for where a codebase-wide interface-versus-alias policy meets reality, and decide whether loose Record parameters at your integration boundaries should be replaced with constrained generics rather than worked around at every call site.

## What the target actually demands `Record<string, unknown>` is an object type with a string index signature — conceptually `{ [key: string]: unknown }`. For a source type to be assignable to it, every property of the source must be compatible with the index signature's value type (`unknown` accepts everything, so that half is trivially satisfied), **and** the source must itself be known to have an index signature. That second requirement is the interesting one. Your `Config` never declares `[key: string]: unknown`, so where does the alias version get one? ## The implicit index signature TypeScript grants an *implicit* index signature to object literal types — the anonymous `{ ... }` types you write inline, and the type aliases that name them. The reasoning is that the compiler has the complete member list in front of it: it can check every member against the index signature's value type once and be done. Nothing can add a property later. An interface gets no such grant. An interface is an **open** declaration: another declaration of the same name — in the same file, elsewhere in the program, or inside an augmentation written by a consumer of your library — contributes additional members to the same type. The compiler therefore cannot conclude that the members it can see are all the members there will ever be, so it refuses to synthesise an index signature that would silently cover unseen properties. ```typescript interface Config { url: string } type ConfigAlias = { url: string }; declare function send(payload: Record<string, unknown>): void; const a: Config = { url: "/api" }; const b: ConfigAlias = { url: "/api" }; // @ts-expect-error index signature is missing in type 'Config' send(a); send(b); // fine ``` The diagnostic on the failing line reports that an index signature for type `string` is missing in type `Config` — wording that is confusing precisely because you never wrote an index signature on either declaration. ## Why this is a *choosing* question, not a trivia question This is the clearest place where the interface-versus-alias decision stops being cosmetic and produces a compile error you have to answer for. It shows up constantly at integration boundaries: serialisers, logging helpers, query-string builders, ORM and HTTP client APIs, and anything else whose parameter is typed as a loose bag of keys. A team that has standardised on `interface` for every shape hits it first, and often "fixes" it with an assertion, which is the worst available option because it discards checking rather than expressing intent. ## The fixes, ranked 1. **Declare the shape as a type alias.** Correct when nothing needs to extend the type and it is not part of a public surface that consumers augment. The implicit index signature comes for free and no extra members are permitted. 2. **Add an explicit index signature** — `interface Config { url: string; [key: string]: unknown }`. Honest and precise, but it changes the type's meaning: the interface now accepts arbitrary extra keys, and every declared member must be compatible with `unknown` (trivial here, but a narrower index value type such as `string` would force every property to be a `string`). 3. **Spread at the call site**: `send({ ...config })`. The spread produces a fresh object literal type, which gets the implicit signature. Cheap, but it copies the object and hides the reason. 4. **Change the parameter's type** if you own it. Often the loose `Record` was never the right parameter type in the first place and a generic constrained to the real shape says more. What you should not do is reach for an assertion such as `config as Record<string, unknown>`. An assertion is not a conversion and performs no check; it silences this diagnostic and every future one at that call site. ## The common wrong explanations Candidates frequently claim interfaces are "nominal" and aliases "structural". Both are structural — a value of an interface type and a value of an identically-shaped alias type are mutually assignable everywhere else. Others claim `Record` only accepts inline object literals; it accepts any type with a compatible index signature. The single fact that explains the behaviour is openness: an interface may grow, so its member list is not final, so no implicit index signature.

  • Why does adding `[key: string]: unknown` to the interface fix it, and what does it cost?
    It supplies explicitly what the compiler refused to infer, so assignability succeeds. The cost is that the interface now genuinely accepts arbitrary extra keys, and every declared member must be compatible with the index signature's value type — with `unknown` that is free, but with a narrower value type it constrains all your properties.
  • Would asserting the argument with `as Record<string, unknown>` be an acceptable fix?
    No. An assertion performs no check and emits no code; it tells the compiler to stop objecting. It hides the real reason, and it will also silence genuine incompatibilities introduced at that call site later. Prefer changing the declaration form or adding the index signature explicitly.
  • Does this same restriction apply to a class instance type?
    Yes — a class declaration does not get an implicit index signature either, for the same reason its member list is not treated as a closed literal. Passing an instance where a `Record` is expected fails in the same way, and the same fixes apply.

saying these in an interview costs you the question

  • Claims interfaces are nominal and aliases structural
  • Says Record only accepts inline object literals
  • Reaches for an assertion as the first fix
  • Thinks the interface is missing a property, not a signature
  • Believes the two declarations produce different runtime objects

context