Skip to content

Commit 24a45d0

Browse files
committed
Fixed mapped type modifier propagation when remapping multiple keys to same property (#62318)
1 parent 2dfdbba commit 24a45d0

File tree

6 files changed

+470
-0
lines changed

6 files changed

+470
-0
lines changed

src/compiler/checker.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14782,6 +14782,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1478214782
if (existingProp) {
1478314783
existingProp.links.nameType = getUnionType([existingProp.links.nameType!, propNameType]);
1478414784
existingProp.links.keyType = getUnionType([existingProp.links.keyType, keyType]);
14785+
const modifiersProp = isTypeUsableAsPropertyName(keyType) ? getPropertyOfType(modifiersType, getPropertyNameFromType(keyType)) : undefined;
14786+
const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional ||
14787+
!(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
14788+
const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly ||
14789+
!(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp));
14790+
if (isOptional && !(existingProp.flags & SymbolFlags.Optional)) {
14791+
existingProp.flags |= SymbolFlags.Optional;
14792+
existingProp.links.checkFlags &= ~CheckFlags.StripOptional;
14793+
}
14794+
if (isReadonly) {
14795+
existingProp.links.checkFlags |= CheckFlags.Readonly;
14796+
}
1478514797
}
1478614798
else {
1478714799
const modifiersProp = isTypeUsableAsPropertyName(keyType) ? getPropertyOfType(modifiersType, getPropertyNameFromType(keyType)) : undefined;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
mappedTypeRemappingModifierMerging.ts(45,5): error TS2540: Cannot assign to 'foo' because it is a read-only property.
2+
mappedTypeRemappingModifierMerging.ts(46,5): error TS2540: Cannot assign to 'foo' because it is a read-only property.
3+
4+
5+
==== mappedTypeRemappingModifierMerging.ts (2 errors) ====
6+
// Mapped types with key remapping should merge modifiers consistently
7+
// when multiple keys map to the same output key
8+
9+
type RemapKeyToInitialPart<T> = {
10+
[K in keyof T as K extends `${infer First}.${infer _Rest}` ? First : K]: null;
11+
};
12+
13+
// Both should produce { foo?: null } since at least one input is optional
14+
type FirstOptional = RemapKeyToInitialPart<{
15+
"foo.bar"?: string;
16+
"foo.baz": number;
17+
}>;
18+
19+
type FirstRequired = RemapKeyToInitialPart<{
20+
"foo.baz": number;
21+
"foo.bar"?: string;
22+
}>;
23+
24+
// Test that they are equivalent
25+
const testOptional: FirstOptional = { foo: null };
26+
const testOptional2: FirstOptional = {};
27+
28+
const testRequired: FirstRequired = { foo: null };
29+
const testRequired2: FirstRequired = {};
30+
31+
// Readonly should work the same way
32+
type RemapWithReadonly<T> = {
33+
[K in keyof T as K extends `${infer First}.${string}` ? First : K]: null;
34+
};
35+
36+
type FirstReadonly = RemapWithReadonly<{
37+
readonly "foo.bar": string;
38+
"foo.baz": number;
39+
}>;
40+
41+
type SecondReadonly = RemapWithReadonly<{
42+
"foo.baz": number;
43+
readonly "foo.bar": string;
44+
}>;
45+
46+
declare const ro1: FirstReadonly;
47+
declare const ro2: SecondReadonly;
48+
49+
// Both should be readonly
50+
ro1.foo = null; // Error
51+
~~~
52+
!!! error TS2540: Cannot assign to 'foo' because it is a read-only property.
53+
ro2.foo = null; // Error
54+
~~~
55+
!!! error TS2540: Cannot assign to 'foo' because it is a read-only property.
56+
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//// [tests/cases/compiler/mappedTypeRemappingModifierMerging.ts] ////
2+
3+
//// [mappedTypeRemappingModifierMerging.ts]
4+
// Mapped types with key remapping should merge modifiers consistently
5+
// when multiple keys map to the same output key
6+
7+
type RemapKeyToInitialPart<T> = {
8+
[K in keyof T as K extends `${infer First}.${infer _Rest}` ? First : K]: null;
9+
};
10+
11+
// Both should produce { foo?: null } since at least one input is optional
12+
type FirstOptional = RemapKeyToInitialPart<{
13+
"foo.bar"?: string;
14+
"foo.baz": number;
15+
}>;
16+
17+
type FirstRequired = RemapKeyToInitialPart<{
18+
"foo.baz": number;
19+
"foo.bar"?: string;
20+
}>;
21+
22+
// Test that they are equivalent
23+
const testOptional: FirstOptional = { foo: null };
24+
const testOptional2: FirstOptional = {};
25+
26+
const testRequired: FirstRequired = { foo: null };
27+
const testRequired2: FirstRequired = {};
28+
29+
// Readonly should work the same way
30+
type RemapWithReadonly<T> = {
31+
[K in keyof T as K extends `${infer First}.${string}` ? First : K]: null;
32+
};
33+
34+
type FirstReadonly = RemapWithReadonly<{
35+
readonly "foo.bar": string;
36+
"foo.baz": number;
37+
}>;
38+
39+
type SecondReadonly = RemapWithReadonly<{
40+
"foo.baz": number;
41+
readonly "foo.bar": string;
42+
}>;
43+
44+
declare const ro1: FirstReadonly;
45+
declare const ro2: SecondReadonly;
46+
47+
// Both should be readonly
48+
ro1.foo = null; // Error
49+
ro2.foo = null; // Error
50+
51+
52+
//// [mappedTypeRemappingModifierMerging.js]
53+
"use strict";
54+
// Mapped types with key remapping should merge modifiers consistently
55+
// when multiple keys map to the same output key
56+
// Test that they are equivalent
57+
var testOptional = { foo: null };
58+
var testOptional2 = {};
59+
var testRequired = { foo: null };
60+
var testRequired2 = {};
61+
// Both should be readonly
62+
ro1.foo = null; // Error
63+
ro2.foo = null; // Error
64+
65+
66+
//// [mappedTypeRemappingModifierMerging.d.ts]
67+
type RemapKeyToInitialPart<T> = {
68+
[K in keyof T as K extends `${infer First}.${infer _Rest}` ? First : K]: null;
69+
};
70+
type FirstOptional = RemapKeyToInitialPart<{
71+
"foo.bar"?: string;
72+
"foo.baz": number;
73+
}>;
74+
type FirstRequired = RemapKeyToInitialPart<{
75+
"foo.baz": number;
76+
"foo.bar"?: string;
77+
}>;
78+
declare const testOptional: FirstOptional;
79+
declare const testOptional2: FirstOptional;
80+
declare const testRequired: FirstRequired;
81+
declare const testRequired2: FirstRequired;
82+
type RemapWithReadonly<T> = {
83+
[K in keyof T as K extends `${infer First}.${string}` ? First : K]: null;
84+
};
85+
type FirstReadonly = RemapWithReadonly<{
86+
readonly "foo.bar": string;
87+
"foo.baz": number;
88+
}>;
89+
type SecondReadonly = RemapWithReadonly<{
90+
"foo.baz": number;
91+
readonly "foo.bar": string;
92+
}>;
93+
declare const ro1: FirstReadonly;
94+
declare const ro2: SecondReadonly;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//// [tests/cases/compiler/mappedTypeRemappingModifierMerging.ts] ////
2+
3+
=== mappedTypeRemappingModifierMerging.ts ===
4+
// Mapped types with key remapping should merge modifiers consistently
5+
// when multiple keys map to the same output key
6+
7+
type RemapKeyToInitialPart<T> = {
8+
>RemapKeyToInitialPart : Symbol(RemapKeyToInitialPart, Decl(mappedTypeRemappingModifierMerging.ts, 0, 0))
9+
>T : Symbol(T, Decl(mappedTypeRemappingModifierMerging.ts, 3, 27))
10+
11+
[K in keyof T as K extends `${infer First}.${infer _Rest}` ? First : K]: null;
12+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 4, 5))
13+
>T : Symbol(T, Decl(mappedTypeRemappingModifierMerging.ts, 3, 27))
14+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 4, 5))
15+
>First : Symbol(First, Decl(mappedTypeRemappingModifierMerging.ts, 4, 39))
16+
>_Rest : Symbol(_Rest, Decl(mappedTypeRemappingModifierMerging.ts, 4, 54))
17+
>First : Symbol(First, Decl(mappedTypeRemappingModifierMerging.ts, 4, 39))
18+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 4, 5))
19+
20+
};
21+
22+
// Both should produce { foo?: null } since at least one input is optional
23+
type FirstOptional = RemapKeyToInitialPart<{
24+
>FirstOptional : Symbol(FirstOptional, Decl(mappedTypeRemappingModifierMerging.ts, 5, 2))
25+
>RemapKeyToInitialPart : Symbol(RemapKeyToInitialPart, Decl(mappedTypeRemappingModifierMerging.ts, 0, 0))
26+
27+
"foo.bar"?: string;
28+
>"foo.bar" : Symbol("foo.bar", Decl(mappedTypeRemappingModifierMerging.ts, 8, 44))
29+
30+
"foo.baz": number;
31+
>"foo.baz" : Symbol("foo.baz", Decl(mappedTypeRemappingModifierMerging.ts, 9, 23))
32+
33+
}>;
34+
35+
type FirstRequired = RemapKeyToInitialPart<{
36+
>FirstRequired : Symbol(FirstRequired, Decl(mappedTypeRemappingModifierMerging.ts, 11, 3))
37+
>RemapKeyToInitialPart : Symbol(RemapKeyToInitialPart, Decl(mappedTypeRemappingModifierMerging.ts, 0, 0))
38+
39+
"foo.baz": number;
40+
>"foo.baz" : Symbol("foo.baz", Decl(mappedTypeRemappingModifierMerging.ts, 13, 44))
41+
42+
"foo.bar"?: string;
43+
>"foo.bar" : Symbol("foo.bar", Decl(mappedTypeRemappingModifierMerging.ts, 14, 22))
44+
45+
}>;
46+
47+
// Test that they are equivalent
48+
const testOptional: FirstOptional = { foo: null };
49+
>testOptional : Symbol(testOptional, Decl(mappedTypeRemappingModifierMerging.ts, 19, 5))
50+
>FirstOptional : Symbol(FirstOptional, Decl(mappedTypeRemappingModifierMerging.ts, 5, 2))
51+
>foo : Symbol(foo, Decl(mappedTypeRemappingModifierMerging.ts, 19, 37))
52+
53+
const testOptional2: FirstOptional = {};
54+
>testOptional2 : Symbol(testOptional2, Decl(mappedTypeRemappingModifierMerging.ts, 20, 5))
55+
>FirstOptional : Symbol(FirstOptional, Decl(mappedTypeRemappingModifierMerging.ts, 5, 2))
56+
57+
const testRequired: FirstRequired = { foo: null };
58+
>testRequired : Symbol(testRequired, Decl(mappedTypeRemappingModifierMerging.ts, 22, 5))
59+
>FirstRequired : Symbol(FirstRequired, Decl(mappedTypeRemappingModifierMerging.ts, 11, 3))
60+
>foo : Symbol(foo, Decl(mappedTypeRemappingModifierMerging.ts, 22, 37))
61+
62+
const testRequired2: FirstRequired = {};
63+
>testRequired2 : Symbol(testRequired2, Decl(mappedTypeRemappingModifierMerging.ts, 23, 5))
64+
>FirstRequired : Symbol(FirstRequired, Decl(mappedTypeRemappingModifierMerging.ts, 11, 3))
65+
66+
// Readonly should work the same way
67+
type RemapWithReadonly<T> = {
68+
>RemapWithReadonly : Symbol(RemapWithReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 23, 40))
69+
>T : Symbol(T, Decl(mappedTypeRemappingModifierMerging.ts, 26, 23))
70+
71+
[K in keyof T as K extends `${infer First}.${string}` ? First : K]: null;
72+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 27, 5))
73+
>T : Symbol(T, Decl(mappedTypeRemappingModifierMerging.ts, 26, 23))
74+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 27, 5))
75+
>First : Symbol(First, Decl(mappedTypeRemappingModifierMerging.ts, 27, 39))
76+
>First : Symbol(First, Decl(mappedTypeRemappingModifierMerging.ts, 27, 39))
77+
>K : Symbol(K, Decl(mappedTypeRemappingModifierMerging.ts, 27, 5))
78+
79+
};
80+
81+
type FirstReadonly = RemapWithReadonly<{
82+
>FirstReadonly : Symbol(FirstReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 28, 2))
83+
>RemapWithReadonly : Symbol(RemapWithReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 23, 40))
84+
85+
readonly "foo.bar": string;
86+
>"foo.bar" : Symbol("foo.bar", Decl(mappedTypeRemappingModifierMerging.ts, 30, 40))
87+
88+
"foo.baz": number;
89+
>"foo.baz" : Symbol("foo.baz", Decl(mappedTypeRemappingModifierMerging.ts, 31, 31))
90+
91+
}>;
92+
93+
type SecondReadonly = RemapWithReadonly<{
94+
>SecondReadonly : Symbol(SecondReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 33, 3))
95+
>RemapWithReadonly : Symbol(RemapWithReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 23, 40))
96+
97+
"foo.baz": number;
98+
>"foo.baz" : Symbol("foo.baz", Decl(mappedTypeRemappingModifierMerging.ts, 35, 41))
99+
100+
readonly "foo.bar": string;
101+
>"foo.bar" : Symbol("foo.bar", Decl(mappedTypeRemappingModifierMerging.ts, 36, 22))
102+
103+
}>;
104+
105+
declare const ro1: FirstReadonly;
106+
>ro1 : Symbol(ro1, Decl(mappedTypeRemappingModifierMerging.ts, 40, 13))
107+
>FirstReadonly : Symbol(FirstReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 28, 2))
108+
109+
declare const ro2: SecondReadonly;
110+
>ro2 : Symbol(ro2, Decl(mappedTypeRemappingModifierMerging.ts, 41, 13))
111+
>SecondReadonly : Symbol(SecondReadonly, Decl(mappedTypeRemappingModifierMerging.ts, 33, 3))
112+
113+
// Both should be readonly
114+
ro1.foo = null; // Error
115+
>ro1.foo : Symbol(foo)
116+
>ro1 : Symbol(ro1, Decl(mappedTypeRemappingModifierMerging.ts, 40, 13))
117+
>foo : Symbol(foo)
118+
119+
ro2.foo = null; // Error
120+
>ro2.foo : Symbol(foo)
121+
>ro2 : Symbol(ro2, Decl(mappedTypeRemappingModifierMerging.ts, 41, 13))
122+
>foo : Symbol(foo)
123+

0 commit comments

Comments
 (0)