Skip to content

Commit 5d021b4

Browse files
authored
Don't reduce 'keyof M' for mapped types with non-distributive 'as' clauses (#41186)
* Don't reduce 'keyof M' for mapped types with non-distributive as clauses * Add regression test * Accept new baselines
1 parent 672861a commit 5d021b4

File tree

5 files changed

+223
-1
lines changed

5 files changed

+223
-1
lines changed

src/compiler/checker.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13475,6 +13475,19 @@ namespace ts {
1347513475
constraint;
1347613476
}
1347713477

13478+
// Ordinarily we reduce a keyof M where M is a mapped type { [P in K as N<P>]: X } to simply N<K>. This however presumes
13479+
// that N distributes over union types, i.e. that N<A | B | C> is equivalent to N<A> | N<B> | N<C>. That presumption is
13480+
// generally true, except when N is a non-distributive conditional type or an instantiable type with non-distributive
13481+
// conditional type as a constituent. In those cases, we cannot reduce keyof M and need to preserve it as is.
13482+
function isNonDistributiveNameType(type: Type | undefined): boolean {
13483+
return !!(type && (
13484+
type.flags & TypeFlags.Conditional && !(<ConditionalType>type).root.isDistributive ||
13485+
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((<UnionOrIntersectionType | TemplateLiteralType>type).types, isNonDistributiveNameType) ||
13486+
type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && isNonDistributiveNameType((<IndexType | StringMappingType>type).type) ||
13487+
type.flags & TypeFlags.IndexedAccess && isNonDistributiveNameType((<IndexedAccessType>type).indexType) ||
13488+
type.flags & TypeFlags.Substitution && isNonDistributiveNameType((<SubstitutionType>type).substitute)));
13489+
}
13490+
1347813491
function getLiteralTypeFromPropertyName(name: PropertyName) {
1347913492
if (isPrivateIdentifier(name)) {
1348013493
return neverType;
@@ -13522,7 +13535,7 @@ namespace ts {
1352213535
type = getReducedType(type);
1352313536
return type.flags & TypeFlags.Union ? getIntersectionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
1352413537
type.flags & TypeFlags.Intersection ? getUnionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
13525-
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
13538+
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && isNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
1352613539
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(<MappedType>type, noIndexSignatures) :
1352713540
type === wildcardType ? wildcardType :
1352813541
type.flags & TypeFlags.Unknown ? neverType :

tests/baselines/reference/mappedTypeAsClauses.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,35 @@ const e1: T1 = {
5757
};
5858
type T2 = keyof T1;
5959
const e2: T2 = "foo";
60+
61+
// Repro from #41133
62+
63+
interface Car {
64+
name: string;
65+
seats: number;
66+
engine: Engine;
67+
wheels: Wheel[];
68+
}
69+
70+
interface Engine {
71+
manufacturer: string;
72+
horsepower: number;
73+
}
74+
75+
interface Wheel {
76+
type: "summer" | "winter";
77+
radius: number;
78+
}
79+
80+
type Primitive = string | number | boolean;
81+
type OnlyPrimitives<T> = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
82+
83+
let primitiveCar: OnlyPrimitives<Car>; // { name: string; seats: number; }
84+
let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
85+
86+
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
87+
88+
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
6089
6190
6291
//// [mappedTypeAsClauses.js]
@@ -66,6 +95,9 @@ var e1 = {
6695
foo: "hello"
6796
};
6897
var e2 = "foo";
98+
var primitiveCar; // { name: string; seats: number; }
99+
var keys; // "name" | "seats"
100+
var carKeys; // "name" | "seats"
69101
70102
71103
//// [mappedTypeAsClauses.d.ts]
@@ -135,3 +167,25 @@ declare type T1 = PickByValueType<Example, string>;
135167
declare const e1: T1;
136168
declare type T2 = keyof T1;
137169
declare const e2: T2;
170+
interface Car {
171+
name: string;
172+
seats: number;
173+
engine: Engine;
174+
wheels: Wheel[];
175+
}
176+
interface Engine {
177+
manufacturer: string;
178+
horsepower: number;
179+
}
180+
interface Wheel {
181+
type: "summer" | "winter";
182+
radius: number;
183+
}
184+
declare type Primitive = string | number | boolean;
185+
declare type OnlyPrimitives<T> = {
186+
[K in keyof T as T[K] extends Primitive ? K : never]: T[K];
187+
};
188+
declare let primitiveCar: OnlyPrimitives<Car>;
189+
declare let keys: keyof OnlyPrimitives<Car>;
190+
declare type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
191+
declare let carKeys: KeysOfPrimitives<Car>;

tests/baselines/reference/mappedTypeAsClauses.symbols

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,79 @@ const e2: T2 = "foo";
188188
>e2 : Symbol(e2, Decl(mappedTypeAsClauses.ts, 57, 5))
189189
>T2 : Symbol(T2, Decl(mappedTypeAsClauses.ts, 55, 2))
190190

191+
// Repro from #41133
192+
193+
interface Car {
194+
>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
195+
196+
name: string;
197+
>name : Symbol(Car.name, Decl(mappedTypeAsClauses.ts, 61, 15))
198+
199+
seats: number;
200+
>seats : Symbol(Car.seats, Decl(mappedTypeAsClauses.ts, 62, 17))
201+
202+
engine: Engine;
203+
>engine : Symbol(Car.engine, Decl(mappedTypeAsClauses.ts, 63, 18))
204+
>Engine : Symbol(Engine, Decl(mappedTypeAsClauses.ts, 66, 1))
205+
206+
wheels: Wheel[];
207+
>wheels : Symbol(Car.wheels, Decl(mappedTypeAsClauses.ts, 64, 19))
208+
>Wheel : Symbol(Wheel, Decl(mappedTypeAsClauses.ts, 71, 1))
209+
}
210+
211+
interface Engine {
212+
>Engine : Symbol(Engine, Decl(mappedTypeAsClauses.ts, 66, 1))
213+
214+
manufacturer: string;
215+
>manufacturer : Symbol(Engine.manufacturer, Decl(mappedTypeAsClauses.ts, 68, 18))
216+
217+
horsepower: number;
218+
>horsepower : Symbol(Engine.horsepower, Decl(mappedTypeAsClauses.ts, 69, 25))
219+
}
220+
221+
interface Wheel {
222+
>Wheel : Symbol(Wheel, Decl(mappedTypeAsClauses.ts, 71, 1))
223+
224+
type: "summer" | "winter";
225+
>type : Symbol(Wheel.type, Decl(mappedTypeAsClauses.ts, 73, 17))
226+
227+
radius: number;
228+
>radius : Symbol(Wheel.radius, Decl(mappedTypeAsClauses.ts, 74, 30))
229+
}
230+
231+
type Primitive = string | number | boolean;
232+
>Primitive : Symbol(Primitive, Decl(mappedTypeAsClauses.ts, 76, 1))
233+
234+
type OnlyPrimitives<T> = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
235+
>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
236+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
237+
>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
238+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
239+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
240+
>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
241+
>Primitive : Symbol(Primitive, Decl(mappedTypeAsClauses.ts, 76, 1))
242+
>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
243+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
244+
>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
245+
246+
let primitiveCar: OnlyPrimitives<Car>; // { name: string; seats: number; }
247+
>primitiveCar : Symbol(primitiveCar, Decl(mappedTypeAsClauses.ts, 81, 3))
248+
>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
249+
>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
250+
251+
let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
252+
>keys : Symbol(keys, Decl(mappedTypeAsClauses.ts, 82, 3))
253+
>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
254+
>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
255+
256+
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
257+
>KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36))
258+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 84, 22))
259+
>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
260+
>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 84, 22))
261+
262+
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
263+
>carKeys : Symbol(carKeys, Decl(mappedTypeAsClauses.ts, 86, 3))
264+
>KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36))
265+
>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
266+

tests/baselines/reference/mappedTypeAsClauses.types

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,53 @@ const e2: T2 = "foo";
120120
>e2 : "foo"
121121
>"foo" : "foo"
122122

123+
// Repro from #41133
124+
125+
interface Car {
126+
name: string;
127+
>name : string
128+
129+
seats: number;
130+
>seats : number
131+
132+
engine: Engine;
133+
>engine : Engine
134+
135+
wheels: Wheel[];
136+
>wheels : Wheel[]
137+
}
138+
139+
interface Engine {
140+
manufacturer: string;
141+
>manufacturer : string
142+
143+
horsepower: number;
144+
>horsepower : number
145+
}
146+
147+
interface Wheel {
148+
type: "summer" | "winter";
149+
>type : "summer" | "winter"
150+
151+
radius: number;
152+
>radius : number
153+
}
154+
155+
type Primitive = string | number | boolean;
156+
>Primitive : Primitive
157+
158+
type OnlyPrimitives<T> = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
159+
>OnlyPrimitives : OnlyPrimitives<T>
160+
161+
let primitiveCar: OnlyPrimitives<Car>; // { name: string; seats: number; }
162+
>primitiveCar : OnlyPrimitives<Car>
163+
164+
let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
165+
>keys : "name" | "seats"
166+
167+
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
168+
>KeysOfPrimitives : keyof OnlyPrimitives<T>
169+
170+
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
171+
>carKeys : "name" | "seats"
172+

tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,32 @@ const e1: T1 = {
5959
};
6060
type T2 = keyof T1;
6161
const e2: T2 = "foo";
62+
63+
// Repro from #41133
64+
65+
interface Car {
66+
name: string;
67+
seats: number;
68+
engine: Engine;
69+
wheels: Wheel[];
70+
}
71+
72+
interface Engine {
73+
manufacturer: string;
74+
horsepower: number;
75+
}
76+
77+
interface Wheel {
78+
type: "summer" | "winter";
79+
radius: number;
80+
}
81+
82+
type Primitive = string | number | boolean;
83+
type OnlyPrimitives<T> = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
84+
85+
let primitiveCar: OnlyPrimitives<Car>; // { name: string; seats: number; }
86+
let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
87+
88+
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
89+
90+
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"

0 commit comments

Comments
 (0)