From 5d021b401aafa74c856324b6cbf8cd54769839cf Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 21 Oct 2020 12:16:46 -0700 Subject: [PATCH] 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 --- src/compiler/checker.ts | 15 +++- .../reference/mappedTypeAsClauses.js | 54 +++++++++++++ .../reference/mappedTypeAsClauses.symbols | 76 +++++++++++++++++++ .../reference/mappedTypeAsClauses.types | 50 ++++++++++++ .../types/mapped/mappedTypeAsClauses.ts | 29 +++++++ 5 files changed, 223 insertions(+), 1 deletion(-) 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"