TypeScript Index Signatures – Everything You Have to Know

A woman picking an index folder from a shelf

It’s not uncommon to get an Index signature for type 'string' is missing in type 'A' error in TypeScript. The error message is pretty straightforward to understand… once you know what an index signature is in the first place – so what are TypeScript index signatures exactly and what are the non-obvious gotchas?

An index signature is a feature that describes a dictionary structure of certain key types and values:

interface A {
  [i: string]: string | number
  a: string
}

declare const a: A;

a.x = ''; // Valid
Code language: PHP (php)

Try It

A has an index signature of [i: string]: string | number which means any index of type string can be accessed on type A and expected to have a value of type string | number.

Regular properties defined on type A work as usual – they can have a narrower type (which must be assignable to the index signature) and any other type must also have those properties defined in order to be assignable to it.

interface A {
  [x: string]: string | number
  a: string
}

interface B {
  [x: string]: string | number
  b: boolean // Error: Property 'b' of type 'boolean' is not assignable to 'string' index type 'string | number'
}

interface C {
  [x: string]: string
}

declare let a: A;
declare let c: C;

// Any property of string type can be accessed on C, but that does not mean the property exists and makes it assignable to type A
a = c; // Error: Property 'a' is missing in type 'C' but required in type 'A'Code language: PHP (php)

Try It

It’s also possible to define a type with an index signature with the following:

interface A extends Record<string, number> { 
  //
}

type B = Record<string, number>;

declare const a: A;
declare const b: B;

a.x = 0;
b.y = 1;Code language: PHP (php)

Try It

Allowed index signature types

An index signature must be of string | number | symbol type. Further, it cannot be a constant like 'a' | 'b' though template literal types are allowed.

interface A {
  [i: boolean]: unknown // Error: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type
}

interface B {
  [i: 'a' | 'b']: unknown // Error: An index signature parameter type cannot be a literal type or generic type.
}

interface C {
  [i: `entry_${string}`]: unknown // Valid
}

// Valid but the index signature is of type [i: string]: unknown and only considered for assignability
type D = Record<'a' | 'b', unknown>;Code language: PHP (php)

Try It

Note that you can still define a Record<'a' | 'b', unknown> and it will be assignable to Record<string, unknown> – it’s not because it has an index signature of 'a' | 'b' – it’s because of how implicit index signatures work.

Implicit index signatures

Something that’s very confusing about index signatures is that TypeScript will treat certain types as having an index signature even if it was not defined and only in relation to assignability.

The implicit index signature is assigned to an object literal type, most commonly created with type A = { } this also includes Record<'a' | 'b', string> – as noted in the previous section. It’s never assigned to interfaces and that’s one of the major differences between the two.

// Has an implicit index signature
type A  = {
  a: string
}

// Does not have any index signature
interface B {
  b: string
}Code language: PHP (php)

Now you’d think since A has an index signature, we could do a.x = ''? No. The implicit index signature is created only in the context of assignability, otherwise there’s no index signature.

type A = { a: string }

interface B { b: string }

interface C extends Record<string, string> { };

interface D extends Record<string, number> { };

declare let a: A;
declare let b: B;
declare let c: C;
declare let d: D;

// The implicit index signature only has an effect for assignability, otherwise it does not exist
a.x = ''; // Error: Property 'x' does not exist on type 'A'
c = a; // Valid

// Interfaces lack the implicit index signature
c = b; // Error - Index signature for type 'string' is missing in type 'B'

// The implicit signature is derived from A's defined properties and their values
d = a; // Error - Type 'string' is not assignable to type 'number'Code language: PHP (php)

Try It

Official definition:

An object literal type is now assignable to a type with an index signature if all known properties in the object literal are assignable to that index signature. This makes it possible to pass a variable that was initialized with an object literal as parameter to a function that expects a map or dictionary.

TypeScript 2.0 Release Notes

Solving the “Index signature is missing” error

One of the most common errors you would get in TypeScript is Index signature for type 'string' is missing in type 'A' (2345) Hopefully this error is not so mysterious anymore. It occurs when we try to assign a type that lacks an index signature such as an interface to a one that does, such as a Record<string, unknown> but how to solve it properly?

I have encountered it often with generic parameters:

interface A {
  a: string;
}

type Constraint<T extends Record<string, unknown>> = true;

type test = Constraint<A>; // Error:  Index signature for type 'string' is missing in type 'A'Code language: PHP (php)

Try It

It would almost feel intuitive that A matches the constraint, but it does not – since it lacks the index signature of type string. The hacky solution would be to define A as an object literal type so its implicit index signature makes it assignable:

type A = {
  a: string;
}

type Constraint<T extends Record<string, unknown>> = true;

type test = Constraint<A>; // ValidCode language: JavaScript (javascript)

Try It

I say hacky, because doing this imposes a certain limitation. You would have an API in your app that can only accept object literal types and not interfaces, and would either need to not use interfaces at all, selectively refactor interfaces to types which is inconsistent or convert interfaces to types before passing them as the input.

The proper solution would be to remove the index signature from the constraint:

interface A {
  a: string
}

type Constraint<T extends Record<keyof T, unknown>> = true;

type test = Constraint<A>; // ValidCode language: PHP (php)

Try It