What's going on with index types in TypeScript?

dwjohnston profile image David Johnston ・2 min read

Playground link for all code here.

In TypeScript you can reference the type of an object property using the square bracket notation.


type Foo = {
   a: string;
   b: number;
   1: null;

type A = Foo["a"]; //string 
type B = Foo["b"]; //number
type ObjOne = Foo[1]; //null; 

This also works for arrays

type MyArray = [string, number, string];

type Zero = MyArray[0]; //string 
type One = MyArray[1]; //number

Now according to the TypeScript documentation the keyof keyword returns a union type of all of those possible keys, which also includes the Array.prototype methods such as reduce, map etc.

type Reduce = MyArray["reduce"]; //type Reduce = {    (callbackfn: (previousValue: 0 | "one" | ..... 
type Length = MyArray["length"] //3

This works for ordinary objects too, but there aren't many useful native methods that exist on an object instance's prototype (Object.values etc exist as static functions on the class prototype):

type ToString = Foo["toString"]; //() => string
type Values = Foo["values"]; //Property 'values' does not exist on type 'Foo'.(2339)

Now where this gets interesting is that as well as referencing the types of properties via a string or number literal as an index - we can also reference them by a given type as an index. However, in this current example, it only works for the number type on arrays only:

type RefObjectByTypeNumber = Foo[number]; //Type 'Foo' has no matching index signature for type 'number'.(2537)
type RefObjectByTypeString = Foo[string]; //Type 'Foo' has no matching index signature for type 'number'.(2537)
type RefArrayByTypeNumber = MyArray[number]; //string | number
type RefArrayByTypeString = MyArray[string]; //Type 'MyArray' has no matching index signature for type 'string'.(2537)

If we declare an object by explicitly declaring the types of the keys, this does work:

type Bar = {
   [key: string]: string,
type RefBarByTypeString = Bar[string]; //string

type Chaz = {[key: number]: number}; 
type RefChazByTypeNumber = Chaz[number]; //number

If we declare the object keys directly as string literals, this does not work:

type Eep = {
   "a": number; 
type RefEepByKeyA = Eep["a"]; //number
type RefEepByTypeString = Eep[string]; // Type 'Eep' has no matching index signature for type 'string'.(2537)

It's not possible to declare both types as keys:

type Donk = {
   [keya: string]: string;
   [keyb: number]: number; //Numeric index type 'number' is not assignable to string index type 'string'.(2413)

Not that I necessarily want to do that.

So my main question here is - what's actually going on with how we can use types as an index - but only in specific circumstances?

The documentation from Typescript isn't particularly clear here - the only references in the documentation that I can find is is this part on index types:

which doesn't actual demonstrate using a type as an index,

and the example given here in the keyof documentation:



Editor guide
macsikora profile image
Maciej Sikora

We have here three separated things.

1.Index type
Its a type accessor by index. So for example User['id'] take a type of property id from User type.
More about index type
2.Index signature type
Its a way to define the type, but limited to index type being string or number. More about that - index signatures

// Indexed type syntax only available for keys being string or number
type Donk = {
  [keya: string]: string;
   // no more rows available
const donk = { 'any_str': 'any_str' }; // string to string object
Enter fullscreen mode Exit fullscreen mode

3.Mapped types
Mapped type is a construction allows create a type by mapping through keys being another type. This is exactly the construct which allows on creating object types with specified keys and values types.

Mapped type example:

type Keys = 'a' | 'b' | 'c'
type Donky = {
   [K in Keys]: string
const donky: Donky = { a: 'a', b: 'b', c:'c' }; // a | b | c to string object
Enter fullscreen mode Exit fullscreen mode

More about mapped type