You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Only T1, T11, and T12 are 1, which makes me think there's some level of unsoundness. Some more mind twisters:
typeT1=Record<"a"|"b",number>extendsRecord<number, infer Textendsstring> ? T : never;typeT2=Record<"a"|"b",number>extendsRecord<number, infer Textendsnumber> ? T : never;typeT3=Record<"a"|"b",number>extendsRecord<number, infer Textends{any: "thing"}> ? T : never;typeT4=Record<"a"|"b",number>extendsRecord<number, infer T> ? T : never;
π Actual behavior
A mapped type with string literal keys (which aren't `${number}`) is assignable to a type with number index signature, with disjoint value types.
π Expected behavior
It should not be assignable. If Record<"a" | "b", number> extends Record<number, string> because the key types are disjoint, then I'd expect the converse to be true as well, but Record<number, string> doesn't extend Record<"a" | "b", number>.
I may have got some idea about it. Because every Record<"a" | "b", number> is a valid Record<number, string> (there's no number keys in it, yet), but almost all Record<number, string> aren't Record<"a" | "b", number> (because there's no guarantee they have both a and b keys). Proof is that both of these are 1:
If the OP showed convincing real and urgent use cases where this was a problem, it would be more interesting.
I got deceived by mapped types and index signatures both having the form of Record, but they are very different entities. Making that realization makes everything sound again. There's no problem here.
Joshua Chen (@Josh-Cena) - You are right on principle. I think it deserves an exact algorithmic description. A rough not-so-elegantly written and possibly not-exactly-correct stab at the algorithm - please correct it if it is wrong:
Given Record<keytypes,any> extends Record<keytypes,any>
Let's not think about it in terms of records, because records can mean two different things: mapped types and index signatures. Record<"a", any> is a mapped type (MT), Record<string, any> is an index signature (IS). For mapped types, every key must exist; for index signatures, there's no key that needs to exist at all: it only restricts what value must be associated with a key of that type. The reason for this approach is because your analysis has dropped index signatures in the form of template literals.
So now we have four worlds: IS extends IS, MT extends IS, IS extends MT, MT extends MT.
IS extends IS
If the two index signatures are disjoint, they are always cross-assignable. The proof is that Record<`a ${string}`, number> extends Record<number, string> and Record<number, string> extends Record<`a ${string}`, number> are both true. (Note the use of template literals to eliminate the case of `${number}`
Otherwise, the LHS's key type and value type must both be subtypes of RHS (covariance). In the position of index signatures, number is also a subtype of string, because `${number}` and number are equivalent. (In fact `${number}` may not be equivalent to number because " 1 " is a valid `${number}`, but I understand the choice of ergonomics)
MT extends IS follows the same rule as above. For example, Record<0 | 1, string> extends Record<`a ${string}`, number>, and Record<"a" | "b", number> extends Record<number, string>
IS extends MT
This will never be true, because there's no guarantee that any key exists in the index signature type, so it can't be assigned to any non-trivial mapped type. This is why Record<number, string> extends Record<"a" | "b", number> is false.
MT extends MT
This is true only is LHS's key type and value type are both subtypes of RHS. They are NOT cross-assignable when their key types are disjoint (contrary to the case where one side is an IS). The rule of string-number equivalence still stands, so Record<0 | 1, string> and Record<"0" | "1", string> are equivalent.
As you can see, out of the four worlds, our familiar covariance principle is always applicable, with a little exception that `${number}` = number in the key position. But when the RHS is an index signature, the rule is relaxed to also allow disjoint key types. My original example is the case (2).
You're right, lets not think about Records. 'Record' is only syntactic sugar for describing common subsets of object types.
Here's an example of an object type that cannot be described using a single Record template expression, although (I think) any object can be expressed as a logical and (&) of nulitple records:
type A = `${number}`;
type B = `${string}${number}`;
type X = {
[k:A]:1|6,
[k:B]:2,
[k:string]:1|2|3|4|5|6,
[k:number]:4|6,
"a":5,
"1":6
}
type Y = Record<A,1|6>&Record<B,2>&Record<string,1|2|3|4|5|6>&Record<number,4|6>&Record<"a",5>&Record<"1",6>
type YeX_true = Y extends X ? true : false;
type XeY_true = X extends Y ? true : false;
const xa:X = {2:1} // Error ... ooops ... shouldn't X be a disallowed type?
const xb:X = {2:4} // Error ... "
I do not understand how your classification fits arbitrary records, like Record<"a"|string,string>, as well as more complex object types like X above.
My previous comment - using Record expressable object types only and leaving out template strings - isn't really good enough, for sure.
type T1 = Record<"a"|number, string>;
type T2 = Record<number, string>;
const a1n1:T1 = {1:"","a":""};
const a2:T2 = {1:"","a":""}; // Type '{ 1: string; a: string; }' is not assignable to type 'T2'.
I would also be wary of saying "exactly the same thing" because the internal rep, after converting from Record, probably still contains the "a" property, which would incurr different code paths. The final result (probably) should be the same, but to prove it is NP-hard.
You said "a"|string originally, your example there is "a"|number which is indeed different from number.
"a"|string immediately simplifies to string (by design, beforeRecord is instantiated) so the distinction is never observable to any of your code. Therefore, as far as your program can tell, they are exactly the same type. Any arguments beyond that are purely philosophical.
typeT1=Record<"a"|number,string>;typeT2=Record<number,string>;consta1n1:T1={1:"","a":""};consta2:T2=a1n1;// ok
The only reason your version has an error is because object literals are checked for excess properties. It is not actually a type error, it is a lint check to prevent you from creating properties that you won't be able to access later.
You said "a"|string originally, your example there is "a"|number which is indeed different from number.
The point was that a mixed literal & index Record exists - "All right then" = "you are correct and here is a correct example" . That's obvious from context. I enjoyed and am absorbing the rest of your comments thought - so thanks for those.
typeT1=Record<"a"|number,string>;typeT2=Record<number,string>;consta1n1:T1={1:"","a":""};consta2:T2=a1n1;// ok
The only reason your version has an error is because object literals are checked for excess properties. It is not actually a type error, it is a lint check to prevent you from creating properties that you won't be able to access later.
Activity
Josh-Cena commentedon May 1, 2022
I may have got some idea about it. Because every
Record<"a" | "b", number>is a validRecord<number, string>(there's no number keys in it, yet), but almost allRecord<number, string>aren'tRecord<"a" | "b", number>(because there's no guarantee they have bothaandbkeys). Proof is that both of these are1:Josh-Cena commentedon May 2, 2022
Direct assignment will be subject to excess property checks. The following works:
I got deceived by mapped types and index signatures both having the form of
Record, but they are very different entities. Making that realization makes everything sound again. There's no problem here.craigphicks commentedon May 2, 2022
Joshua Chen (@Josh-Cena) - You are right on principle. I think it deserves an exact algorithmic description. A rough not-so-elegantly written and possibly not-exactly-correct stab at the algorithm - please correct it if it is wrong:
Given Record<keytypes,any> extends Record<keytypes,any>
stringgoto PASS; // string always passesnumbergoto PASS; // on the rhs, number is just an alias for stringstring{ somepass=true; break; }numberand !isNaN(Number(LKR) { somepass=true; break; }When the types are not any ... any, this extra condition is logically AND-ed
For reference:
I think your question and answer was actually pretty useful, although being a question, it's not really an issue.
Josh-Cena commentedon May 3, 2022
Let's not think about it in terms of records, because records can mean two different things: mapped types and index signatures.
Record<"a", any>is a mapped type (MT),Record<string, any>is an index signature (IS). For mapped types, every key must exist; for index signatures, there's no key that needs to exist at all: it only restricts what value must be associated with a key of that type. The reason for this approach is because your analysis has dropped index signatures in the form of template literals.So now we have four worlds: IS extends IS, MT extends IS, IS extends MT, MT extends MT.
Record<`a ${string}`, number> extends Record<number, string>andRecord<number, string> extends Record<`a ${string}`, number>are both true. (Note the use of template literals to eliminate the case of`${number}`numberis also a subtype ofstring, because`${number}`andnumberare equivalent. (In fact`${number}`may not be equivalent tonumberbecause" 1 "is a valid`${number}`, but I understand the choice of ergonomics)Record<0 | 1, string> extends Record<`a ${string}`, number>, andRecord<"a" | "b", number> extends Record<number, string>Record<number, string> extends Record<"a" | "b", number>is false.Record<0 | 1, string>andRecord<"0" | "1", string>are equivalent.As you can see, out of the four worlds, our familiar covariance principle is always applicable, with a little exception that
`${number}`=numberin the key position. But when the RHS is an index signature, the rule is relaxed to also allow disjoint key types. My original example is the case (2).craigphicks commentedon May 4, 2022
Joshua Chen (@Josh-Cena) -
You're right, lets not think about Records. 'Record' is only syntactic sugar for describing common subsets of object types.
Here's an example of an object type that cannot be described using a single Record template expression, although (I think) any object can be expressed as a logical and (&) of nulitple records:
I do not understand how your classification fits arbitrary records, like
Record<"a"|string,string>, as well as more complex object types likeXabove.My previous comment - using Record expressable object types only and leaving out template strings - isn't really good enough, for sure.
fatcerberus commentedon May 6, 2022
Record<"a" | string, string>is exactly the same thing asRecord<string, string>craigphicks commentedon May 6, 2022
Bruce Pascoe (@fatcerberus) - All right then
I would also be wary of saying "exactly the same thing" because the internal rep, after converting from Record, probably still contains the "a" property, which would incurr different code paths. The final result (probably) should be the same, but to prove it is NP-hard.
fatcerberus commentedon May 7, 2022
You said
"a"|stringoriginally, your example there is"a"|numberwhich is indeed different fromnumber."a"|stringimmediately simplifies tostring(by design, beforeRecordis instantiated) so the distinction is never observable to any of your code. Therefore, as far as your program can tell, they are exactly the same type. Any arguments beyond that are purely philosophical.fatcerberus commentedon May 7, 2022
I'd also like to point out this:
The only reason your version has an error is because object literals are checked for excess properties. It is not actually a type error, it is a lint check to prevent you from creating properties that you won't be able to access later.
craigphicks commentedon May 7, 2022
The point was that a mixed literal & index Record exists - "All right then" = "you are correct and here is a correct example" . That's obvious from context. I enjoyed and am absorbing the rest of your comments thought - so thanks for those.
craigphicks commentedon May 7, 2022
Correct. I didn't mean to claim otherwise.
craigphicks commentedon May 7, 2022
Bruce Pascoe (@fatcerberus)
FWIW in the TypeScript compiler environment I created a a new test file
tests/cases/compiler/mytest.tswith content:an ran
gulp runtests --test=mytestto generate the filetests/baselines/local/mytest.typeswith content:which adequately demonstrates the truth of your statements
and