Check nested conditional types for non-distributiveness in mapped types with 'as' clauses (#41713)

* Check nested conditional types for non-distributiveness

* Rename to maybeNonDistributiveNameType

* Add regression test
This commit is contained in:
Anders Hejlsberg 2020-12-01 14:07:47 -08:00 committed by GitHub
parent 4782c74b75
commit 4d6947ae14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 11 deletions

View File

@ -13547,17 +13547,17 @@ namespace ts {
constraint;
}
// 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
// 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
// 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<P>]: X }, to simply N<K>. This however presumes
// that N distributes over union types, i.e. that N<A | B | C> is equivalent to N<A> | N<B> | N<C>. 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 && !(<ConditionalType>type).root.isDistributive ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((<UnionOrIntersectionType | TemplateLiteralType>type).types, isNonDistributiveNameType) ||
type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && isNonDistributiveNameType((<IndexType | StringMappingType>type).type) ||
type.flags & TypeFlags.IndexedAccess && isNonDistributiveNameType((<IndexedAccessType>type).indexType) ||
type.flags & TypeFlags.Substitution && isNonDistributiveNameType((<SubstitutionType>type).substitute)));
type.flags & TypeFlags.Conditional && (!(<ConditionalType>type).root.isDistributive || maybeNonDistributiveNameType((<ConditionalType>type).checkType)) ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((<UnionOrIntersectionType | TemplateLiteralType>type).types, maybeNonDistributiveNameType) ||
type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && maybeNonDistributiveNameType((<IndexType | StringMappingType>type).type) ||
type.flags & TypeFlags.IndexedAccess && maybeNonDistributiveNameType((<IndexedAccessType>type).indexType) ||
type.flags & TypeFlags.Substitution && maybeNonDistributiveNameType((<SubstitutionType>type).substitute)));
}
function getLiteralTypeFromPropertyName(name: PropertyName) {
@ -13607,7 +13607,7 @@ namespace ts {
type = getReducedType(type);
return type.flags & TypeFlags.Union ? getIntersectionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((<IntersectionType>type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && isNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && maybeNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(<MappedType>type, noIndexSignatures) :
type === wildcardType ? wildcardType :
type.flags & TypeFlags.Unknown ? neverType :

View File

@ -86,6 +86,34 @@ let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
// Repro from #41453
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;
type GetKey<S, V> = keyof { [TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any };
type GetKeyWithIf<S, V> = keyof { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
type GetObjWithIf<S, V> = { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
type Task = {
isDone: boolean;
};
type Schema = {
root: {
title: string;
task: Task;
}
Task: Task;
};
type Res1 = GetKey<Schema, Schema['root']['task']>; // "Task"
type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>; // "Task"
type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>; // "Task"
//// [mappedTypeAsClauses.js]
@ -189,3 +217,27 @@ declare let primitiveCar: OnlyPrimitives<Car>;
declare let keys: keyof OnlyPrimitives<Car>;
declare type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
declare let carKeys: KeysOfPrimitives<Car>;
declare type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
declare type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;
declare type GetKey<S, V> = keyof {
[TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any;
};
declare type GetKeyWithIf<S, V> = keyof {
[TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any;
};
declare type GetObjWithIf<S, V> = {
[TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any;
};
declare type Task = {
isDone: boolean;
};
declare type Schema = {
root: {
title: string;
task: Task;
};
Task: Task;
};
declare type Res1 = GetKey<Schema, Schema['root']['task']>;
declare type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>;
declare type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>;

View File

@ -264,3 +264,108 @@ let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
>KeysOfPrimitives : Symbol(KeysOfPrimitives, Decl(mappedTypeAsClauses.ts, 82, 36))
>Car : Symbol(Car, Decl(mappedTypeAsClauses.ts, 57, 21))
// Repro from #41453
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => 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 boolean, Then, Else> = 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<S, V> = keyof { [TP in keyof S as Equal<S[TP], V> 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<S, V> = keyof { [TP in keyof S as If<Equal<S[TP], V>, 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<S, V> = { [TP in keyof S as If<Equal<S[TP], V>, 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<Schema, Schema['root']['task']>; // "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<Schema, Schema['root']['task']>; // "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<Schema, Schema['root']['task']>; // "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))

View File

@ -170,3 +170,58 @@ type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
>carKeys : "name" | "seats"
// Repro from #41453
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
>Equal : Equal<A, B>
>true : true
>false : false
type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;
>If : If<Cond, Then, Else>
>true : true
type GetKey<S, V> = keyof { [TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any };
>GetKey : keyof { [TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any; }
>true : true
type GetKeyWithIf<S, V> = keyof { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
>GetKeyWithIf : keyof { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any; }
type GetObjWithIf<S, V> = { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
>GetObjWithIf : GetObjWithIf<S, V>
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<Schema, Schema['root']['task']>; // "Task"
>Res1 : "Task"
type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>; // "Task"
>Res2 : "Task"
type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>; // "Task"
>Res3 : "Task"

View File

@ -88,3 +88,31 @@ let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
// Repro from #41453
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;
type GetKey<S, V> = keyof { [TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any };
type GetKeyWithIf<S, V> = keyof { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
type GetObjWithIf<S, V> = { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
type Task = {
isDone: boolean;
};
type Schema = {
root: {
title: string;
task: Task;
}
Task: Task;
};
type Res1 = GetKey<Schema, Schema['root']['task']>; // "Task"
type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>; // "Task"
type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>; // "Task"