Introduction

An object in JavaScript can be accessed with a string to hold a reference to any other JavaScript object.

let foo: any = {};
foo['Hello'] = 'World';
console.log(foo['Hello']); // World
class Foo {
  constructor(public message: string){};
  log(){
    console.log(this.message)
  }
}
 
let foo: any = {};
foo['Hello'] = new Foo('World');
foo['Hello'].log(); // World
let obj = {
  toString(){
    console.log('toString called')
    return 'Hello'
  }
}
 
let foo: any = {};
foo[obj] = 'World'; // toString called
console.log(foo[obj]); // toString called, World
console.log(foo['Hello']); // World

Syntax

Declaring an index signature

let foo:{ [index:string] : {message: string} } = {};
 
/**
 * Must store stuff that conforms to the structure
 */
/** Ok */
foo['a'] = { message: 'some message' };
/** Error: must contain a `message` of type string. You have a typo in `message` */
foo['a'] = { messages: 'some message' };
 
/**
 * Stuff that is read is also type checked
 */
/** Ok */
foo['a'].message;
/** Error: messages does not exist. You have a typo in `message` */
foo['a'].messages;

Interfaces

As soon as you have a string index signature, all explicit members must also conform to that index signature. A few Interface examples:

interface Foo {
  x: number;
}
let foo: Foo = {x:1,y:2};

// Directly
foo['x']; // number

// Indirectly
let x = 'x'
foo[x]; // number

String literals

String Literals can also be used in the declaration:

type Index = 'a' | 'b' | 'c'
type FromIndex = { [k in Index]?: number }
 
const good: FromIndex = {b:1, c:2}
 
// Error:
// Type '{ b: number; c: number; d: number; }' is not assignable to type 'FromIndex'.
// Object literal may only specify known properties, and 'd' does not exist in type 'FromIndex'.
const bad: FromIndex = {b:1, c:2, d:3};

Generics

Generics can also be used:

type FromSomeIndex<K extends string> = { [key in K]: number }