Repository navigation
mapped type with remapped keys unexpectedly widens type #57265
Description
Activity
The UX problem that we can recognize here is that all of those 3 display in the same way (
{ [k: string]: unknown; }):type A = { [k: string]: unknown } type B = Record<string, unknown> type C = { [K in keyof B as K]: unknown }
However, they are not functionally identical and that's the surprising behavior.
keyof B(a mapped type without thenameType) just gets theconstraintTypeof the mapped type (sostringhere) and returns that as-is butC(a renaming mapped type) behaves likeA(even though it's still a mapped type internally, it's not like it's eagerly resolved to a plain object type).The UX problem that we can recognize here is that all of those 3 display in the same way (
{ [k: string]: unknown; }):type A = { [k: string]: unknown } type B = Record<string, unknown> type C = { [K in keyof B as K]: unknown }
However, they are not functionally identical and that's the surprising behavior.
keyof B(a mapped type without thenameType) just gets theconstraintTypeof the mapped type (sostringhere) and returns that as-is butC(a renaming mapped type) behaves likeA(even though it's still a mapped type internally, it's not like it's eagerly resolved to a plain object type).Yes there is a UX problem here that two different types are presented the same and behave differently, and that's definitely a problem.
It's still a mystery to me is WHY the key gets widened in the first place from
stringtostring | number. This was clearly intentional, as mentioned in the 2.9 release notes and the PR you linked://
keyofcurrently always returnsstring | numberfor concretestringindex signatures - the below ternary keeps that behavior for mapped typesThe keys of an object are never numbers - they're always strings. Property access by number coerces the number to a string, both at the type level and at the value level. Why does
keyofperform the reverse widening instead of just letting the forward coercion deal with this when accessing the property?I think the discussion at #41966 bears on this, and I wonder if #43041 (An alternative option to
keyofStringsOnlythat stringifies numeric properties and index signatures) can be prioritized, given thatkeyofStringsOnlyis now deprecated.Here's my trying to understand:
// @target: ES2022 const r = {0:'zero', 1:'one', 2:'two'} // two similar record types type R1 = Record<string, string> type R2 = {[s:string]:string} // both types are indexable by number const v1 = (r as R1)[0] const v2 = (r as R2)[0] // and have values indexable by number type V1 = R1['0'] // ^? type V2 = R2['0'] // ^? // and for-in types the iterated keys to be strings for (const k in (r as R1)){} // ^? for (const k in (r as R2)){} // ^? // but `keyof` differs between the types: type K1 = keyof R1 // ^? type K2 = keyof R2 // ^?
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Feb 2, 2024 The keys of an object are never numbers - they're always strings. Property access by number coerces the number to a string, both at the type level and at the value level. Why does keyof perform the reverse widening instead of just letting the forward coercion deal with this when accessing the property?
This is true, but note they're not completely unified at the type level - types can have separate
stringandnumberindex signatures, the former is just required to subsume the latter if both exist on the same type (for obvious reasons).keyof Xjust tells you what kind of value you're allowed to put in between the brackets, for which the most accurate answer isstring | number). Note that this actually makesRecord<string, T>the odd one out - as it only hasstringfor itskeyof- but I believe that was done to preserve the natural contravariance of the key type)This is true, but note they're not completely unified at the type level - types can have separate
stringandnumberindex signatures, the former is just required to subsume the latter if both exist on the same type (for obvious reasons).keyof Xjust tells you what kind of value you're allowed to put in between the brackets, for which the most accurate answer isstring | number)That's one way to think of it. But I don't think it really makes sense.
keyofis most useful for object literals, and havingkeyof {1:null}be1 | "1"would be quite ugly. It's nice thatkeyofis in direct correspondence with keys in object literals.- Numbers are not "special" for bracketing. Other primitives like
nullortrueare just as valid as1as an object key, but we don't include them when the key type isstring. - The
stringandnumberindex signatures don't have to be in correspondence - only if they're declared in an object literal (which is. For funsies I made a whole set of pathological examples!
// @target:ES2022 declare const o : {[x:number]:'number'}&{[x:string]:'string'}&{[x:`${number}`]:'numstring'} const p1 = o[1] // ^? const p2 = o['1'] // ^? const p3 = o['one'] // ^? const p4 = o['0x0'] // ^? const p5 = o['Infinity'] // ^? const p6 = o[`${300n}`] // ^? const p7 = o['123_456'] // ^? const p8 = o[123_456] // ^? const p9 = o['-1'] // ^? const p10 = o['+1'] // ^? const p11 = o[`${[6]}`] // ^?
Workbench Repro
(fixed link)- addedHas ReproThis issue has compiler-backed repros: https://aka.ms/ts-reprosThis issue has compiler-backed repros: https://aka.ms/ts-repros
on Feb 4, 2024 The string and number index signatures don't have to be in correspondence - only if they're declared in an object literal (which is. For funsies I made a whole set of pathological examples!
Fun little technicality there: Those pathological cases actually only exist because of a design limitation. The type
{ [x: number]: 'number', [x: string]: 'string' }is not actually a legal type but for one little quirk: type instantiation (including the construction of an intersection type) is not allowed to fail. As a result, you can construct such types via intersection, but they're generally not well-behaved. It's for the same reason that you can write things like{ [x: string]: string } & { foo: number }--which are also not well-behaved; see e.g. discussion at #17867.Fun little technicality there: Those pathological cases actually only exist because of a design limitation. The type
{ [x: number]: 'number', [x: string]: 'string' }is not actually a legal type but for one little quirk: type instantiation (including the construction of an intersection type) is not allowed to fail.The inconsistency does not require use of intersection types to see:
declare const o : { [x:string]:'string'|'number'|'numstring', [x:number]:'number'|'numstring', [x:`${number}`]:'numstring' } // rest stay the same
And that design limitation does not excuse/explain the behavior. TypeScript could treat the type as
neveras it does for{x:1} & {x:'one'}or infer the property to beneveras it does for 3 of those 11 cases (no spoilers).typescript-bot commented
on Feb 15, 2024 ContributorMore actions👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.
Issue body code block by Dan Rose (@rotu)
‼️ Exception: Error - error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information.Error: error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information. at Object.createVirtualTypeScriptEnvironment (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:8128:11) at twoslasher (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:7618:17) at /home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:439:44 at runTwoslashRequests (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:406:56) at run (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:20096:75) at process.processTicksAndRejections (node:internal/process/task_queues:95:5)Historical Information
Version Reproduction Outputs Time 4.9.3, 5.0.2, 5.1.3, 5.2.2, 5.3.2 ❌ Failed: -
Type 'string | number' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'.
⚠️ Way slowertypescript-bot commented
on Feb 15, 2024 ContributorMore actions👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of this repro running against the nightly TypeScript.
‼️ Exception: Error - error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information.Error: error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information. at Object.createVirtualTypeScriptEnvironment (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:8128:11) at twoslasher (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:7618:17) at /home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:439:44 at runTwoslashRequests (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:406:56) at run (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:20096:75) at process.processTicksAndRejections (node:internal/process/task_queues:95:5)Historical Information
Version Reproduction Outputs Time 4.9.3, 5.0.2, 5.1.3, 5.2.2, 5.3.2 ⚠️ Assertions:type V1 = stringtype V2 = stringconst k: stringconst k: stringtype K1 = stringtype K2 = string | number
⚠️ Way slowertypescript-bot commented
on Feb 15, 2024 ContributorMore actions👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of this repro running against the nightly TypeScript.
‼️ Exception: Error - error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information.Error: error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error. Visit https://aka.ms/ts6 for migration information. at Object.createVirtualTypeScriptEnvironment (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:8128:11) at twoslasher (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:7618:17) at /home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:439:44 at runTwoslashRequests (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:406:56) at run (/home/runner/work/_actions/microsoft/TypeScript-Twoslash-Repro-Action/master/dist/index.js:20096:75) at process.processTicksAndRejections (node:internal/process/task_queues:95:5)Historical Information
Version Reproduction Outputs Time 4.9.3, 5.0.2, 5.1.3, 5.2.2, 5.3.2 ⚠️ Assertions:const p1: "number"const p2: neverconst p3: "string"const p4: "numstring"const p5: "number"const p6: neverconst p7: "string"const p8: "number"const p9: neverconst p10: "numstring"const p11: "string"
⚠️ Way slowerI am currently solving this problem temporarily by defining a
Keys<T>type tool.type Keys<T> = { [P in keyof T as any]: P }[keyof T] type R1 = Keys<Record<string,unknown>> // string type R2 = Keys<{ [x: string]: unknown }> // string
🔎 Search Terms
keyof, string, record
🕗 Version & Regression Information
⏯ Playground Link
No response
💻 Code
Workbench Repro
🙁 Actual behavior
keyof Risstring | number.🙂 Expected behavior
keyof Risstring.Additional information about the issue
No response