47

I cannot figure out the difference between index signatures and record types. Could someone explain the differences and when to use one versus the other?

Specifically, I am looking to define the type of an object that will have random strings for its keys and values, which will be iterated over.

is there a difference between:

let objectVariable: Record<string, string> = {}

and

let objectVariable2: {[index: string]: string} = {}
mkusps
  • 520
  • 1
  • 4
  • 7
  • Not alot of difference from my understanding - the `Record` interface is a predefined conviennce/helper – Dacre Denny Jan 08 '19 at 21:37
  • 4
    `Record<>` is a mapped type, so you can do more interesting things with it, like `Record<"foo" | "bar", string>`... see: https://stackoverflow.com/questions/51936369/what-is-the-record-type-in-typescript – Aaron Beall Jan 08 '19 at 21:48

2 Answers2

54

The definition for Record is:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

When creating a type like type MyType = Record<string, string>;, inlining Record leads to the following type:

type MyType = {
    [P in string]: string;
};

This is saying to create an object type with string property names within the set string. Since string is unbounded there's unlimited possibilities of strings (unlike a union of string literal types like "prop1" | "prop2")... so it's describing an object that can have any number of properties with any name, with the only restriction being that the properties must have a type of string.

So yes, from a type checking perspective it's basically equivalent to the example with the index signature without a mapped type ({ [index: string]: string; }.

Use a plain index signature

Using Record in this fashion is a little strange though and many people might not understand what's going on. A more common way to express intent when there can be any number of properties, is to not use a mapped type:

type ObjectWithStringProperties = {
    [index: string]: string;
};

This has the added benefit of helping explain what the key is supposed to be. For example:

type PersonsByName = {
    [name: string]: Person;
};
const collection: PersonsByName = {};

Note that in this way the types are different because a developer using an object with this type will have this extra described key name information to look at in their editor.

Using Record

Note that Record is usually used like the following:

type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>;
// goes to...
type ThreeStringProps = { [P in "prop1" | "prop2" | "prop3"]: string; };
// goes to...
type ThreeStringProps = {
    prop1: string;
    prop2: string;
    prop3: string;
};
David Sherret
  • 101,669
  • 28
  • 188
  • 178
9

Whether it is a good idea to use Record instead of a plain index signature may be a matter of debate (as David Shereet points out in his answer). Also the fact that you can do a lot more thing with Record then you can with a simple index signature is also something that should be mentioned.

The main part of this question (in my reading) is whether the two types are the same. They are obviously declared in different ways but are they the same type. While they are obviously compatible (that is you can assign one to the other and vice-versa) the question is are there corner cases where this is not possible.

While it's hard to find an exhaustive list of what you can do with a type, Matt McCutchen in this answer provides an interesting type that detects weather the readonly modifier is present (something that simple compatibility does not detect the difference between). I would surmise that if Record and an index signature are the considered the same in the way Matt uses them there (as part of the signature of a generic function) they are pretty much the same type declared in a different way:

type IfEquals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? "Y" : "N";

let same : IfEquals<{x: string}, {x: string}>= "Y"
let notsame : IfEquals<{ y: string }, { x: string }>= "N"
let notsamero: IfEquals<{ readonly x: string }, { x: string }> = "N"
let samerecord: IfEquals<{ [x: string]:string }, Record<string, string>> = "Y"

As we can see in the last example the type of samerecord is Y meaning that the compiler treated the two types as being the same thing. Thus I would surmise { [x: string]:string } and Record<string, string> are exactly the same thing.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 2
    "you can do a lot more thing with Record then you can with a simple index signature" -- can you give an example of something you can do with Record you can't do with an index signature? Thanks! – Chris Haines Feb 06 '20 at 10:12
  • @ChrisHaines `Record<"A" | "B", boolean>` to get the type `{ A: boolean, B: boolean }`. Or `Record` to get a type with the same keys as `T` but of type `boolean`, just as examples – Titian Cernicova-Dragomir Feb 06 '20 at 11:14
  • This can also be achieved with an index signature like so: `{ [key: keyof "A" | "B"]: boolean }` – Chris Haines Feb 07 '20 at 12:06
  • 3
    @ChrisHaines No it can't, index signature parameters MUST be `string` or `number` (not even a union of the two works). You can use a mapped `{ [key in "A" | "B"]: boolean }`, but that really is just writing out what `Record<"A" | "B", boolean>` really is. – Titian Cernicova-Dragomir Feb 07 '20 at 12:27
  • Apologies, wrong syntax; didn't have an editor on me. But my point is you can express it as an index signature still, so whilst I much prefer the cleaner syntax of `Record`, I'm not convinced "you can do a lot more thing with Record then you can with a simple index signature". – Chris Haines Feb 07 '20 at 12:51
  • 1
    @ChrisHaines you are confusing an index signatures with a mapped types. While they have similar syntax, they are different concepts. – Titian Cernicova-Dragomir Feb 07 '20 at 14:02
  • 3
    Another difference is that index syntax works with recursive structures https://www.typescriptlang.org/play?#code/C4TwDgpgBAUgzgewHZQLy0Ug2gXSgHygG8osBrCEALijmACcBLJAcxxvmSgF8CoAjBAgA2EAIYpCSAK4BbfhHp86TVnxnDhAbiA and Record does not https://www.typescriptlang.org/play?#code/C4TwDgpgBAUgzgewHZQLy0Ug2gXSgHygCUIBjBAJwBMAeOYCgSyQHMAaDZAPgKgCMECADYQAhikJIArgFs+ECr3pNWvaUKEBuIA – Gajus Jan 14 '21 at 02:16
  • The issue with using `Record` in a recursive type is that TS does not allow recursive type aliases, however you can define your record as a mapped type to achieve the same thing. https://www.typescriptlang.org/play?#code/C4TwDgpgBAUgzgewHYCEBOCDWElQLyyKobZIDaAulAD5QBKEAxgmgCYA8cwaAlkgOYAaQsnRYcAPhpQARggQAbCAENctJAFcAtjIhppXXgOmaFCgNwAoAPTWoAUQCOGngDdlSpMCjAEPgBbQynKuEMIyGt5IfsxaYArKfMBwUABmGFpQACrgEADKjLxgwJagkCK4BPDIlNIA3lBk2CBQfFCGfPwUAFwVUAC+0nKKKmpQmjp6BtydJhpm5kA – portalguy15837 Jan 24 '23 at 23:42