diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ebb2b79af90..4ed611a4663 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13547,17 +13547,17 @@ 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 { + // 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 may not + // be true when N is a non-distributive conditional type or an instantiable type with a non-distributive conditional type as + // a constituent. In those cases, we cannot reduce keyof M and need to preserve it as is. + function maybeNonDistributiveNameType(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))); + type.flags & TypeFlags.Conditional && (!(type).root.isDistributive || maybeNonDistributiveNameType((type).checkType)) || + type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((type).types, maybeNonDistributiveNameType) || + type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && maybeNonDistributiveNameType((type).type) || + type.flags & TypeFlags.IndexedAccess && maybeNonDistributiveNameType((type).indexType) || + type.flags & TypeFlags.Substitution && maybeNonDistributiveNameType((type).substitute))); } function getLiteralTypeFromPropertyName(name: PropertyName) { @@ -13607,7 +13607,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) || isGenericMappedType(type) && isNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(type, stringsOnly) : + type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && maybeNonDistributiveNameType(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 1940283e299..e5e15d8e6b2 100644 --- a/tests/baselines/reference/mappedTypeAsClauses.js +++ b/tests/baselines/reference/mappedTypeAsClauses.js @@ -86,6 +86,34 @@ let keys: keyof OnlyPrimitives; // "name" | "seats" type KeysOfPrimitives = keyof OnlyPrimitives; let carKeys: KeysOfPrimitives; // "name" | "seats" + +// Repro from #41453 + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +type If = Cond extends true ? Then : Else; + +type GetKey = keyof { [TP in keyof S as Equal extends true ? TP : never]: any }; + +type GetKeyWithIf = keyof { [TP in keyof S as If, TP, never>]: any }; + +type GetObjWithIf = { [TP in keyof S as If, TP, never>]: any }; + +type Task = { + isDone: boolean; +}; + +type Schema = { + root: { + title: string; + task: Task; + } + Task: Task; +}; + +type Res1 = GetKey; // "Task" +type Res2 = GetKeyWithIf; // "Task" +type Res3 = keyof GetObjWithIf; // "Task" //// [mappedTypeAsClauses.js] @@ -189,3 +217,27 @@ declare let primitiveCar: OnlyPrimitives; declare let keys: keyof OnlyPrimitives; declare type KeysOfPrimitives = keyof OnlyPrimitives; declare let carKeys: KeysOfPrimitives; +declare type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +declare type If = Cond extends true ? Then : Else; +declare type GetKey = keyof { + [TP in keyof S as Equal extends true ? TP : never]: any; +}; +declare type GetKeyWithIf = keyof { + [TP in keyof S as If, TP, never>]: any; +}; +declare type GetObjWithIf = { + [TP in keyof S as If, TP, never>]: any; +}; +declare type Task = { + isDone: boolean; +}; +declare type Schema = { + root: { + title: string; + task: Task; + }; + Task: Task; +}; +declare type Res1 = GetKey; +declare type Res2 = GetKeyWithIf; +declare type Res3 = keyof GetObjWithIf; diff --git a/tests/baselines/reference/mappedTypeAsClauses.symbols b/tests/baselines/reference/mappedTypeAsClauses.symbols index 77ca6856b10..1de577a5fd5 100644 --- a/tests/baselines/reference/mappedTypeAsClauses.symbols +++ b/tests/baselines/reference/mappedTypeAsClauses.symbols @@ -264,3 +264,108 @@ let carKeys: KeysOfPrimitives; // "name" | "seats" >KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36)) >Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21)) +// Repro from #41453 + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +>Equal : Symbol(Equal, Decl(mappedTypeAsClauses.ts, 86, 35)) +>A : Symbol(A, Decl(mappedTypeAsClauses.ts, 90, 11)) +>B : Symbol(B, Decl(mappedTypeAsClauses.ts, 90, 13)) +>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 90, 21)) +>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 90, 21)) +>A : Symbol(A, Decl(mappedTypeAsClauses.ts, 90, 11)) +>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 90, 60)) +>T : Symbol(T, Decl(mappedTypeAsClauses.ts, 90, 60)) +>B : Symbol(B, Decl(mappedTypeAsClauses.ts, 90, 13)) + +type If = Cond extends true ? Then : Else; +>If : Symbol(If, Decl(mappedTypeAsClauses.ts, 90, 104)) +>Cond : Symbol(Cond, Decl(mappedTypeAsClauses.ts, 92, 8)) +>Then : Symbol(Then, Decl(mappedTypeAsClauses.ts, 92, 29)) +>Else : Symbol(Else, Decl(mappedTypeAsClauses.ts, 92, 35)) +>Cond : Symbol(Cond, Decl(mappedTypeAsClauses.ts, 92, 8)) +>Then : Symbol(Then, Decl(mappedTypeAsClauses.ts, 92, 29)) +>Else : Symbol(Else, Decl(mappedTypeAsClauses.ts, 92, 35)) + +type GetKey = keyof { [TP in keyof S as Equal extends true ? TP : never]: any }; +>GetKey : Symbol(GetKey, Decl(mappedTypeAsClauses.ts, 92, 76)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 94, 12)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 94, 14)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 94, 29)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 94, 12)) +>Equal : Symbol(Equal, Decl(mappedTypeAsClauses.ts, 86, 35)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 94, 12)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 94, 29)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 94, 14)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 94, 29)) + +type GetKeyWithIf = keyof { [TP in keyof S as If, TP, never>]: any }; +>GetKeyWithIf : Symbol(GetKeyWithIf, Decl(mappedTypeAsClauses.ts, 94, 96)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 96, 18)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 96, 20)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 96, 35)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 96, 18)) +>If : Symbol(If, Decl(mappedTypeAsClauses.ts, 90, 104)) +>Equal : Symbol(Equal, Decl(mappedTypeAsClauses.ts, 86, 35)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 96, 18)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 96, 35)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 96, 20)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 96, 35)) + +type GetObjWithIf = { [TP in keyof S as If, TP, never>]: any }; +>GetObjWithIf : Symbol(GetObjWithIf, Decl(mappedTypeAsClauses.ts, 96, 91)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 98, 18)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 98, 20)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 98, 29)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 98, 18)) +>If : Symbol(If, Decl(mappedTypeAsClauses.ts, 90, 104)) +>Equal : Symbol(Equal, Decl(mappedTypeAsClauses.ts, 86, 35)) +>S : Symbol(S, Decl(mappedTypeAsClauses.ts, 98, 18)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 98, 29)) +>V : Symbol(V, Decl(mappedTypeAsClauses.ts, 98, 20)) +>TP : Symbol(TP, Decl(mappedTypeAsClauses.ts, 98, 29)) + +type Task = { +>Task : Symbol(Task, Decl(mappedTypeAsClauses.ts, 98, 85)) + + isDone: boolean; +>isDone : Symbol(isDone, Decl(mappedTypeAsClauses.ts, 100, 13)) + +}; + +type Schema = { +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) + + root: { +>root : Symbol(root, Decl(mappedTypeAsClauses.ts, 104, 15)) + + title: string; +>title : Symbol(title, Decl(mappedTypeAsClauses.ts, 105, 9)) + + task: Task; +>task : Symbol(task, Decl(mappedTypeAsClauses.ts, 106, 18)) +>Task : Symbol(Task, Decl(mappedTypeAsClauses.ts, 98, 85)) + } + Task: Task; +>Task : Symbol(Task, Decl(mappedTypeAsClauses.ts, 108, 3)) +>Task : Symbol(Task, Decl(mappedTypeAsClauses.ts, 98, 85)) + +}; + +type Res1 = GetKey; // "Task" +>Res1 : Symbol(Res1, Decl(mappedTypeAsClauses.ts, 110, 2)) +>GetKey : Symbol(GetKey, Decl(mappedTypeAsClauses.ts, 92, 76)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) + +type Res2 = GetKeyWithIf; // "Task" +>Res2 : Symbol(Res2, Decl(mappedTypeAsClauses.ts, 112, 51)) +>GetKeyWithIf : Symbol(GetKeyWithIf, Decl(mappedTypeAsClauses.ts, 94, 96)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) + +type Res3 = keyof GetObjWithIf; // "Task" +>Res3 : Symbol(Res3, Decl(mappedTypeAsClauses.ts, 113, 57)) +>GetObjWithIf : Symbol(GetObjWithIf, Decl(mappedTypeAsClauses.ts, 96, 91)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) +>Schema : Symbol(Schema, Decl(mappedTypeAsClauses.ts, 102, 2)) + diff --git a/tests/baselines/reference/mappedTypeAsClauses.types b/tests/baselines/reference/mappedTypeAsClauses.types index 5ada8893a6f..55bb1c780d0 100644 --- a/tests/baselines/reference/mappedTypeAsClauses.types +++ b/tests/baselines/reference/mappedTypeAsClauses.types @@ -170,3 +170,58 @@ type KeysOfPrimitives = keyof OnlyPrimitives; let carKeys: KeysOfPrimitives; // "name" | "seats" >carKeys : "name" | "seats" +// Repro from #41453 + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +>Equal : Equal +>true : true +>false : false + +type If = Cond extends true ? Then : Else; +>If : If +>true : true + +type GetKey = keyof { [TP in keyof S as Equal extends true ? TP : never]: any }; +>GetKey : keyof { [TP in keyof S as Equal extends true ? TP : never]: any; } +>true : true + +type GetKeyWithIf = keyof { [TP in keyof S as If, TP, never>]: any }; +>GetKeyWithIf : keyof { [TP in keyof S as If, TP, never>]: any; } + +type GetObjWithIf = { [TP in keyof S as If, TP, never>]: any }; +>GetObjWithIf : GetObjWithIf + +type Task = { +>Task : Task + + isDone: boolean; +>isDone : boolean + +}; + +type Schema = { +>Schema : Schema + + root: { +>root : { title: string; task: Task; } + + title: string; +>title : string + + task: Task; +>task : Task + } + Task: Task; +>Task : Task + +}; + +type Res1 = GetKey; // "Task" +>Res1 : "Task" + +type Res2 = GetKeyWithIf; // "Task" +>Res2 : "Task" + +type Res3 = keyof GetObjWithIf; // "Task" +>Res3 : "Task" + diff --git a/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts b/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts index 747307f3652..6c13786d81d 100644 --- a/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts +++ b/tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts @@ -88,3 +88,31 @@ let keys: keyof OnlyPrimitives; // "name" | "seats" type KeysOfPrimitives = keyof OnlyPrimitives; let carKeys: KeysOfPrimitives; // "name" | "seats" + +// Repro from #41453 + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +type If = Cond extends true ? Then : Else; + +type GetKey = keyof { [TP in keyof S as Equal extends true ? TP : never]: any }; + +type GetKeyWithIf = keyof { [TP in keyof S as If, TP, never>]: any }; + +type GetObjWithIf = { [TP in keyof S as If, TP, never>]: any }; + +type Task = { + isDone: boolean; +}; + +type Schema = { + root: { + title: string; + task: Task; + } + Task: Task; +}; + +type Res1 = GetKey; // "Task" +type Res2 = GetKeyWithIf; // "Task" +type Res3 = keyof GetObjWithIf; // "Task"