diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 601948a3ea0..019527f4303 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -13475,6 +13475,19 @@ namespace ts {
constraint;
}
+ // Ordinarily we reduce a keyof M where M is a mapped type { [P in K as N
]: X } to simply N. This however presumes
+ // that N distributes over union types, i.e. that N is equivalent to N | N | N. That presumption is
+ // generally true, except when N is a non-distributive conditional type or an instantiable type with non-distributive
+ // conditional type as a constituent. In those cases, we cannot reduce keyof M and need to preserve it as is.
+ function isNonDistributiveNameType(type: Type | undefined): boolean {
+ return !!(type && (
+ type.flags & TypeFlags.Conditional && !(type).root.isDistributive ||
+ type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((type).types, isNonDistributiveNameType) ||
+ type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && isNonDistributiveNameType((type).type) ||
+ type.flags & TypeFlags.IndexedAccess && isNonDistributiveNameType((type).indexType) ||
+ type.flags & TypeFlags.Substitution && isNonDistributiveNameType((type).substitute)));
+ }
+
function getLiteralTypeFromPropertyName(name: PropertyName) {
if (isPrivateIdentifier(name)) {
return neverType;
@@ -13522,7 +13535,7 @@ namespace ts {
type = getReducedType(type);
return type.flags & TypeFlags.Union ? getIntersectionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
- type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) ? getIndexTypeForGenericType(type, stringsOnly) :
+ type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && isNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(type, stringsOnly) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type, noIndexSignatures) :
type === wildcardType ? wildcardType :
type.flags & TypeFlags.Unknown ? neverType :
diff --git a/tests/baselines/reference/mappedTypeAsClauses.js b/tests/baselines/reference/mappedTypeAsClauses.js
index 9b97033306d..1940283e299 100644
--- a/tests/baselines/reference/mappedTypeAsClauses.js
+++ b/tests/baselines/reference/mappedTypeAsClauses.js
@@ -57,6 +57,35 @@ const e1: T1 = {
};
type T2 = keyof T1;
const e2: T2 = "foo";
+
+// Repro from #41133
+
+interface Car {
+ name: string;
+ seats: number;
+ engine: Engine;
+ wheels: Wheel[];
+}
+
+interface Engine {
+ manufacturer: string;
+ horsepower: number;
+}
+
+interface Wheel {
+ type: "summer" | "winter";
+ radius: number;
+}
+
+type Primitive = string | number | boolean;
+type OnlyPrimitives = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
+
+let primitiveCar: OnlyPrimitives; // { name: string; seats: number; }
+let keys: keyof OnlyPrimitives; // "name" | "seats"
+
+type KeysOfPrimitives = keyof OnlyPrimitives;
+
+let carKeys: KeysOfPrimitives; // "name" | "seats"
//// [mappedTypeAsClauses.js]
@@ -66,6 +95,9 @@ var e1 = {
foo: "hello"
};
var e2 = "foo";
+var primitiveCar; // { name: string; seats: number; }
+var keys; // "name" | "seats"
+var carKeys; // "name" | "seats"
//// [mappedTypeAsClauses.d.ts]
@@ -135,3 +167,25 @@ declare type T1 = PickByValueType;
declare const e1: T1;
declare type T2 = keyof T1;
declare const e2: T2;
+interface Car {
+ name: string;
+ seats: number;
+ engine: Engine;
+ wheels: Wheel[];
+}
+interface Engine {
+ manufacturer: string;
+ horsepower: number;
+}
+interface Wheel {
+ type: "summer" | "winter";
+ radius: number;
+}
+declare type Primitive = string | number | boolean;
+declare type OnlyPrimitives = {
+ [K in keyof T as T[K] extends Primitive ? K : never]: T[K];
+};
+declare let primitiveCar: OnlyPrimitives;
+declare let keys: keyof OnlyPrimitives;
+declare type KeysOfPrimitives = keyof OnlyPrimitives;
+declare let carKeys: KeysOfPrimitives;
diff --git a/tests/baselines/reference/mappedTypeAsClauses.symbols b/tests/baselines/reference/mappedTypeAsClauses.symbols
index c37212d9658..77ca6856b10 100644
--- a/tests/baselines/reference/mappedTypeAsClauses.symbols
+++ b/tests/baselines/reference/mappedTypeAsClauses.symbols
@@ -188,3 +188,79 @@ const e2: T2 = "foo";
>e2 : Symbol(e2, Decl(mappedTypeAsClauses.ts, 57, 5))
>T2 : Symbol(T2, Decl(mappedTypeAsClauses.ts, 55, 2))
+// Repro from #41133
+
+interface Car {
+>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
+
+ name: string;
+>name : Symbol(Car.name, Decl(mappedTypeAsClauses.ts, 61, 15))
+
+ seats: number;
+>seats : Symbol(Car.seats, Decl(mappedTypeAsClauses.ts, 62, 17))
+
+ engine: Engine;
+>engine : Symbol(Car.engine, Decl(mappedTypeAsClauses.ts, 63, 18))
+>Engine : Symbol(Engine, Decl(mappedTypeAsClauses.ts, 66, 1))
+
+ wheels: Wheel[];
+>wheels : Symbol(Car.wheels, Decl(mappedTypeAsClauses.ts, 64, 19))
+>Wheel : Symbol(Wheel, Decl(mappedTypeAsClauses.ts, 71, 1))
+}
+
+interface Engine {
+>Engine : Symbol(Engine, Decl(mappedTypeAsClauses.ts, 66, 1))
+
+ manufacturer: string;
+>manufacturer : Symbol(Engine.manufacturer, Decl(mappedTypeAsClauses.ts, 68, 18))
+
+ horsepower: number;
+>horsepower : Symbol(Engine.horsepower, Decl(mappedTypeAsClauses.ts, 69, 25))
+}
+
+interface Wheel {
+>Wheel : Symbol(Wheel, Decl(mappedTypeAsClauses.ts, 71, 1))
+
+ type: "summer" | "winter";
+>type : Symbol(Wheel.type, Decl(mappedTypeAsClauses.ts, 73, 17))
+
+ radius: number;
+>radius : Symbol(Wheel.radius, Decl(mappedTypeAsClauses.ts, 74, 30))
+}
+
+type Primitive = string | number | boolean;
+>Primitive : Symbol(Primitive, Decl(mappedTypeAsClauses.ts, 76, 1))
+
+type OnlyPrimitives = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
+>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
+>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
+>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
+>Primitive : Symbol(Primitive, Decl(mappedTypeAsClauses.ts, 76, 1))
+>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 79, 20))
+>K : Symbol(K, Decl(mappedTypeAsClauses.ts, 79, 28))
+
+let primitiveCar: OnlyPrimitives; // { name: string; seats: number; }
+>primitiveCar : Symbol(primitiveCar, Decl(mappedTypeAsClauses.ts, 81, 3))
+>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
+>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
+
+let keys: keyof OnlyPrimitives; // "name" | "seats"
+>keys : Symbol(keys, Decl(mappedTypeAsClauses.ts, 82, 3))
+>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
+>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
+
+type KeysOfPrimitives = keyof OnlyPrimitives;
+>KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 84, 22))
+>OnlyPrimitives : Symbol(OnlyPrimitives, Decl(mappedTypeAsClauses.ts, 78, 43))
+>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 84, 22))
+
+let carKeys: KeysOfPrimitives; // "name" | "seats"
+>carKeys : Symbol(carKeys, Decl(mappedTypeAsClauses.ts, 86, 3))
+>KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36))
+>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
+
diff --git a/tests/baselines/reference/mappedTypeAsClauses.types b/tests/baselines/reference/mappedTypeAsClauses.types
index d0a2cc5e809..5ada8893a6f 100644
--- a/tests/baselines/reference/mappedTypeAsClauses.types
+++ b/tests/baselines/reference/mappedTypeAsClauses.types
@@ -120,3 +120,53 @@ const e2: T2 = "foo";
>e2 : "foo"
>"foo" : "foo"
+// Repro from #41133
+
+interface Car {
+ name: string;
+>name : string
+
+ seats: number;
+>seats : number
+
+ engine: Engine;
+>engine : Engine
+
+ wheels: Wheel[];
+>wheels : Wheel[]
+}
+
+interface Engine {
+ manufacturer: string;
+>manufacturer : string
+
+ horsepower: number;
+>horsepower : number
+}
+
+interface Wheel {
+ type: "summer" | "winter";
+>type : "summer" | "winter"
+
+ radius: number;
+>radius : number
+}
+
+type Primitive = string | number | boolean;
+>Primitive : Primitive
+
+type OnlyPrimitives = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
+>OnlyPrimitives : OnlyPrimitives
+
+let primitiveCar: OnlyPrimitives; // { name: string; seats: number; }
+>primitiveCar : OnlyPrimitives
+
+let keys: keyof OnlyPrimitives; // "name" | "seats"
+>keys : "name" | "seats"
+
+type KeysOfPrimitives = keyof OnlyPrimitives;
+>KeysOfPrimitives : keyof OnlyPrimitives
+
+let carKeys: KeysOfPrimitives; // "name" | "seats"
+>carKeys : "name" | "seats"
+
diff --git a/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts b/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts
index 095d8ebc2cc..747307f3652 100644
--- a/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts
+++ b/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts
@@ -59,3 +59,32 @@ const e1: T1 = {
};
type T2 = keyof T1;
const e2: T2 = "foo";
+
+// Repro from #41133
+
+interface Car {
+ name: string;
+ seats: number;
+ engine: Engine;
+ wheels: Wheel[];
+}
+
+interface Engine {
+ manufacturer: string;
+ horsepower: number;
+}
+
+interface Wheel {
+ type: "summer" | "winter";
+ radius: number;
+}
+
+type Primitive = string | number | boolean;
+type OnlyPrimitives = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
+
+let primitiveCar: OnlyPrimitives; // { name: string; seats: number; }
+let keys: keyof OnlyPrimitives; // "name" | "seats"
+
+type KeysOfPrimitives = keyof OnlyPrimitives;
+
+let carKeys: KeysOfPrimitives; // "name" | "seats"