Why does type Record sometimes complain about missing keys and sometimes not?

Viewed 1393

Consider the following code:

const testOne: Record<"foo"|"bar", string> = {
    "foo": "xyz"
};
const testTwo: Record<string, string> = {
    "foo": "xyz"
};

The first example causes an error that property "bar" is missing. The second example does not cause an error. This confuses me because I'm trying to understand whether Record is a type that implies an existing property for all possible values of its key type or not.

If Record is meant to be a type that does not demand all possible keys to actually exist in a value of that type, then the first example should not cause an error.

If Record is meant to be a type that demands all possible keys to actually exist in a value of that type, then the second example should cause an error too. In this case it would be impossible to construct a value of that type because the set of possible keys is infinite.

If there is a third alternative -- which there seems to be according to what really happens when I try to compile the example -- what is it? The main difference I can see between the two key types is that one has a finite set of values, the other has an infinite set of values. Is this used as the distinction?

Other than that, the only explanation I can find is that Record makes a distinction not only based on the set of values of its key type, but some other property of its key type too. If so, what property of the key type does make the difference? Or does Record do the type system equivalent of "bypass the interface, cast to the implementation type and do something you're not supposed to do"?

The implementation of Record is

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

I can spot two things here. The first is the bound on K to "keyof any", but to my knowledge this limits what types can be used for K, not what values are valid for the resulting type. Second, we have a normal index signature, so my guess would be that what I am confused about in Record is actually the behaviour of index signatures -- but I could not reproduce this behavior without Record easily due to other problems, so I don't want to jump to conclusions.

1 Answers

Let's start from the implementation:


type Record<K extends keyof any, T> = {
    [P in K]: T;
};

keyof any - just means all allowed types that can be used as a key for any object. Now, for such purpose you can use PropertyKey built in type.

{[P in K]: T;} this is just a regular for..in loop.

Hence, when you pass union type "foo"|"bar" as a first argument to the Record, TS compiler just iterates through each union and creates smth like that:

type Result = {
    foo:string,
    bar: string,
}

This means that your final object should have minimum set of properties: foo and bar.

But, when you just passing string as a first argument, things go different.

type Result = Record<string, string>

type Result2 = {
    [P in string]: string
}

As you might have noticed, Result and Result2 are the same types.

Now, you might think that Record<string, string> is equal to indexed interface:

interface Indexed {
    [prop: string]: string
}

type Result = Record<string, string>

type Check = Result extends Indexed ? true : false // true
type Check2 =  Indexed extends Result  ? true : false // true

But these types behaviour is a bit different. See this answer

UPDATE

The question still seems to be whether Record demands all possible properties to actually exist. If Record does not demand all possible properties to actually exist, why is the result an object type with required fields instead of optional fields?

Please see the Mapped Types docs

From the docs:

Mapped types build on the syntax for index signatures, which are used to declare the types of properties which has not been declared ahead of time:

Hence, type Record<string, string>, means that you don't know exactly which keys you will use for the record, but you 100% sure that it will be a string. This is by design.

Why Partial<Record<string, string>> is not the same as Record<string, string>, because Partial means that value can be undefined as well.

In other words, Partial<Record<string, string>> is equal to Record<string,string | undefined>

how does it work for "inifite" types such as string at all

It means that if key is string type, you can use any string to accomplish this requirement. There are no any infinity set of strings.

Related