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

1

11

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.

Justinjustina answered 23/7, 2021 at 6:24 Comment(0)
M
6

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.

Misrepresent answered 23/7, 2021 at 6:51 Comment(3)
While the difference between a Record and an indexed interface is interesting on its own, the main issue seems to be in how a union type is treated differently from "string". The question still seems to be whether Record demands all possible properties to actually exist. If this is not the case for string, why is it the case for a union type? I.e. 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? Not saying it should be, but rather pointing out that the equivalent happens for key type "string".Justinjustina
I'm beginning to understand the difference -- it seems to stem from using [Property in K] instead of [Property: K]. The latter does not work because index signatures for string and number keys must be declared separately. Since "in K" (as used in Record) seems to be useful because of the key enumeration, how does it work for "inifite" types such as string at all? This seems to be relevant to why the requirement for all keys to be present is dropped for string keys.Justinjustina
I found #62882233 which gives another hint: mapped types leave primitive types alone because that makes them most useful. From the beginning I wondered what difference between a union-of-literals and the primitive type string makes the difference, so I can "understand it for the general case", but I now realized that string-y types can only be made from literals, primitive "string", and combinations using operators. There simply is no "general case". I think that answers the question for me.Justinjustina

© 2022 - 2024 — McMap. All rights reserved.