Skip to content

Commit 73736d9

Browse files
authored
Fix logic for determining whether to simplify keyof on mapped types (microsoft#44042)
* Fix logic for determining whether to simplify keyof on mapped types * Add regression test * Improve hasDistributiveNameType check * Add more tests * Address code review feedback * Add more tests
1 parent 87c5b6a commit 73736d9

File tree

6 files changed

+553
-12
lines changed

6 files changed

+553
-12
lines changed

src/compiler/checker.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14333,16 +14333,22 @@ namespace ts {
1433314333
}
1433414334

1433514335
// 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
14336-
// 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
14337-
// be true when N is a non-distributive conditional type or an instantiable type with a non-distributive conditional type as
14338-
// a constituent. In those cases, we cannot reduce keyof M and need to preserve it as is.
14339-
function maybeNonDistributiveNameType(type: Type | undefined): boolean {
14340-
return !!(type && (
14341-
type.flags & TypeFlags.Conditional && (!(type as ConditionalType).root.isDistributive || maybeNonDistributiveNameType((type as ConditionalType).checkType)) ||
14342-
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && some((type as UnionOrIntersectionType | TemplateLiteralType).types, maybeNonDistributiveNameType) ||
14343-
type.flags & (TypeFlags.Index | TypeFlags.StringMapping) && maybeNonDistributiveNameType((type as IndexType | StringMappingType).type) ||
14344-
type.flags & TypeFlags.IndexedAccess && maybeNonDistributiveNameType((type as IndexedAccessType).indexType) ||
14345-
type.flags & TypeFlags.Substitution && maybeNonDistributiveNameType((type as SubstitutionType).substitute)));
14336+
// that N distributes over union types, i.e. that N<A | B | C> is equivalent to N<A> | N<B> | N<C>. Specifically, we only
14337+
// want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable
14338+
// introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because
14339+
// they're the same type regardless of what's being distributed over.
14340+
function hasDistributiveNameType(mappedType: MappedType) {
14341+
const typeVariable = getTypeParameterFromMappedType(mappedType);
14342+
return isDistributive(getNameTypeFromMappedType(mappedType) || typeVariable);
14343+
function isDistributive(type: Type): boolean {
14344+
return type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Primitive | TypeFlags.Never | TypeFlags.TypeParameter | TypeFlags.Object | TypeFlags.NonPrimitive) ? true :
14345+
type.flags & TypeFlags.Conditional ? (type as ConditionalType).root.isDistributive && (type as ConditionalType).checkType === typeVariable :
14346+
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) ? every((type as UnionOrIntersectionType | TemplateLiteralType).types, isDistributive) :
14347+
type.flags & TypeFlags.IndexedAccess ? isDistributive((type as IndexedAccessType).objectType) && isDistributive((type as IndexedAccessType).indexType) :
14348+
type.flags & TypeFlags.Substitution ? isDistributive((type as SubstitutionType).substitute) :
14349+
type.flags & TypeFlags.StringMapping ? isDistributive((type as StringMappingType).type) :
14350+
false;
14351+
}
1434614352
}
1434714353

1434814354
function getLiteralTypeFromPropertyName(name: PropertyName) {
@@ -14393,9 +14399,9 @@ namespace ts {
1439314399
function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
1439414400
const includeOrigin = stringsOnly === keyofStringsOnly && !noIndexSignatures;
1439514401
type = getReducedType(type);
14396-
return type.flags & TypeFlags.Union ? getIntersectionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
14402+
return type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
1439714403
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
14398-
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && maybeNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
14404+
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
1439914405
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, noIndexSignatures) :
1440014406
type === wildcardType ? wildcardType :
1440114407
type.flags & TypeFlags.Unknown ? neverType :
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts(130,3): error TS2345: Argument of type '"a"' is not assignable to parameter of type '"b"'.
2+
3+
4+
==== tests/cases/conformance/types/mapped/mappedTypeAsClauses.ts (1 errors) ====
5+
// Mapped type 'as N' clauses
6+
7+
type Getters<T> = { [P in keyof T & string as `get${Capitalize<P>}`]: () => T[P] };
8+
type TG1 = Getters<{ foo: string, bar: number, baz: { z: boolean } }>;
9+
10+
// Mapped type with 'as N' clause has no constraint on 'in T' clause
11+
12+
type PropDef<K extends keyof any, T> = { name: K, type: T };
13+
14+
type TypeFromDefs<T extends PropDef<keyof any, any>> = { [P in T as P['name']]: P['type'] };
15+
16+
type TP1 = TypeFromDefs<{ name: 'a', type: string } | { name: 'b', type: number } | { name: 'a', type: boolean }>;
17+
18+
// No array or tuple type mapping when 'as N' clause present
19+
20+
type TA1 = Getters<string[]>;
21+
type TA2 = Getters<[number, boolean]>;
22+
23+
// Filtering using 'as N' clause
24+
25+
type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
26+
type TM1 = Methods<{ foo(): number, bar(x: string): boolean, baz: string | number }>;
27+
28+
// Mapping to multiple names using 'as N' clause
29+
30+
type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
31+
type TD1 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
32+
type TD2 = keyof TD1; // 'a1' | 'a2' | 'b1' | 'b2'
33+
type TD3<U> = keyof DoubleProp<U>; // `${keyof U & string}1` | `${keyof U & string}2`
34+
35+
// Repro from #40619
36+
37+
type Lazyify<T> = {
38+
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
39+
};
40+
41+
interface Person {
42+
readonly name: string;
43+
age: number;
44+
location?: string;
45+
}
46+
47+
type LazyPerson = Lazyify<Person>;
48+
49+
// Repro from #40833
50+
51+
type Example = {foo: string, bar: number};
52+
53+
type PickByValueType<T, U> = {
54+
[K in keyof T as T[K] extends U ? K : never]: T[K]
55+
};
56+
57+
type T1 = PickByValueType<Example, string>;
58+
const e1: T1 = {
59+
foo: "hello"
60+
};
61+
type T2 = keyof T1;
62+
const e2: T2 = "foo";
63+
64+
// Repro from #41133
65+
66+
interface Car {
67+
name: string;
68+
seats: number;
69+
engine: Engine;
70+
wheels: Wheel[];
71+
}
72+
73+
interface Engine {
74+
manufacturer: string;
75+
horsepower: number;
76+
}
77+
78+
interface Wheel {
79+
type: "summer" | "winter";
80+
radius: number;
81+
}
82+
83+
type Primitive = string | number | boolean;
84+
type OnlyPrimitives<T> = { [K in keyof T as T[K] extends Primitive ? K : never]: T[K] };
85+
86+
let primitiveCar: OnlyPrimitives<Car>; // { name: string; seats: number; }
87+
let keys: keyof OnlyPrimitives<Car>; // "name" | "seats"
88+
89+
type KeysOfPrimitives<T> = keyof OnlyPrimitives<T>;
90+
91+
let carKeys: KeysOfPrimitives<Car>; // "name" | "seats"
92+
93+
// Repro from #41453
94+
95+
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
96+
97+
type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;
98+
99+
type GetKey<S, V> = keyof { [TP in keyof S as Equal<S[TP], V> extends true ? TP : never]: any };
100+
101+
type GetKeyWithIf<S, V> = keyof { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
102+
103+
type GetObjWithIf<S, V> = { [TP in keyof S as If<Equal<S[TP], V>, TP, never>]: any };
104+
105+
type Task = {
106+
isDone: boolean;
107+
};
108+
109+
type Schema = {
110+
root: {
111+
title: string;
112+
task: Task;
113+
}
114+
Task: Task;
115+
};
116+
117+
type Res1 = GetKey<Schema, Schema['root']['task']>; // "Task"
118+
type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>; // "Task"
119+
type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>; // "Task"
120+
121+
// Repro from #44019
122+
123+
type KeysExtendedBy<T, U> = keyof { [K in keyof T as U extends T[K] ? K : never] : T[K] };
124+
125+
interface M {
126+
a: boolean;
127+
b: number;
128+
}
129+
130+
function f(x: KeysExtendedBy<M, number>) {
131+
return x;
132+
}
133+
134+
f("a"); // Error, should allow only "b"
135+
~~~
136+
!!! error TS2345: Argument of type '"a"' is not assignable to parameter of type '"b"'.
137+
138+
type NameMap = { 'a': 'x', 'b': 'y', 'c': 'z' };
139+
140+
// Distributive, will be simplified
141+
142+
type TS0<T> = keyof { [P in keyof T as keyof Record<P, number>]: string };
143+
type TS1<T> = keyof { [P in keyof T as Extract<P, 'a' | 'b' | 'c'>]: string };
144+
type TS2<T> = keyof { [P in keyof T as P & ('a' | 'b' | 'c')]: string };
145+
type TS3<T> = keyof { [P in keyof T as Exclude<P, 'a' | 'b' | 'c'>]: string };
146+
type TS4<T> = keyof { [P in keyof T as NameMap[P & keyof NameMap]]: string };
147+
type TS5<T> = keyof { [P in keyof T & keyof NameMap as NameMap[P]]: string };
148+
type TS6<T, U, V> = keyof { [ K in keyof T as V & (K extends U ? K : never)]: string };
149+
150+
// Non-distributive, won't be simplified
151+
152+
type TN0<T> = keyof { [P in keyof T as T[P] extends number ? P : never]: string };
153+
type TN1<T> = keyof { [P in keyof T as number extends T[P] ? P : never]: string };
154+
type TN2<T> = keyof { [P in keyof T as 'a' extends P ? 'x' : 'y']: string };
155+
type TN3<T> = keyof { [P in keyof T as Exclude<Exclude<Exclude<P, 'c'>, 'b'>, 'a'>]: string };
156+
type TN4<T, U> = keyof { [K in keyof T as (K extends U ? T[K] : never) extends T[K] ? K : never]: string };
157+
type TN5<T, U> = keyof { [K in keyof T as keyof { [P in K as T[P] extends U ? K : never]: true }]: string };
158+

tests/baselines/reference/mappedTypeAsClauses.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,42 @@ type Schema = {
114114
type Res1 = GetKey<Schema, Schema['root']['task']>; // "Task"
115115
type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>; // "Task"
116116
type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>; // "Task"
117+
118+
// Repro from #44019
119+
120+
type KeysExtendedBy<T, U> = keyof { [K in keyof T as U extends T[K] ? K : never] : T[K] };
121+
122+
interface M {
123+
a: boolean;
124+
b: number;
125+
}
126+
127+
function f(x: KeysExtendedBy<M, number>) {
128+
return x;
129+
}
130+
131+
f("a"); // Error, should allow only "b"
132+
133+
type NameMap = { 'a': 'x', 'b': 'y', 'c': 'z' };
134+
135+
// Distributive, will be simplified
136+
137+
type TS0<T> = keyof { [P in keyof T as keyof Record<P, number>]: string };
138+
type TS1<T> = keyof { [P in keyof T as Extract<P, 'a' | 'b' | 'c'>]: string };
139+
type TS2<T> = keyof { [P in keyof T as P & ('a' | 'b' | 'c')]: string };
140+
type TS3<T> = keyof { [P in keyof T as Exclude<P, 'a' | 'b' | 'c'>]: string };
141+
type TS4<T> = keyof { [P in keyof T as NameMap[P & keyof NameMap]]: string };
142+
type TS5<T> = keyof { [P in keyof T & keyof NameMap as NameMap[P]]: string };
143+
type TS6<T, U, V> = keyof { [ K in keyof T as V & (K extends U ? K : never)]: string };
144+
145+
// Non-distributive, won't be simplified
146+
147+
type TN0<T> = keyof { [P in keyof T as T[P] extends number ? P : never]: string };
148+
type TN1<T> = keyof { [P in keyof T as number extends T[P] ? P : never]: string };
149+
type TN2<T> = keyof { [P in keyof T as 'a' extends P ? 'x' : 'y']: string };
150+
type TN3<T> = keyof { [P in keyof T as Exclude<Exclude<Exclude<P, 'c'>, 'b'>, 'a'>]: string };
151+
type TN4<T, U> = keyof { [K in keyof T as (K extends U ? T[K] : never) extends T[K] ? K : never]: string };
152+
type TN5<T, U> = keyof { [K in keyof T as keyof { [P in K as T[P] extends U ? K : never]: true }]: string };
117153
118154
119155
//// [mappedTypeAsClauses.js]
@@ -126,6 +162,10 @@ var e2 = "foo";
126162
var primitiveCar; // { name: string; seats: number; }
127163
var keys; // "name" | "seats"
128164
var carKeys; // "name" | "seats"
165+
function f(x) {
166+
return x;
167+
}
168+
f("a"); // Error, should allow only "b"
129169
130170
131171
//// [mappedTypeAsClauses.d.ts]
@@ -241,3 +281,57 @@ declare type Schema = {
241281
declare type Res1 = GetKey<Schema, Schema['root']['task']>;
242282
declare type Res2 = GetKeyWithIf<Schema, Schema['root']['task']>;
243283
declare type Res3 = keyof GetObjWithIf<Schema, Schema['root']['task']>;
284+
declare type KeysExtendedBy<T, U> = keyof {
285+
[K in keyof T as U extends T[K] ? K : never]: T[K];
286+
};
287+
interface M {
288+
a: boolean;
289+
b: number;
290+
}
291+
declare function f(x: KeysExtendedBy<M, number>): "b";
292+
declare type NameMap = {
293+
'a': 'x';
294+
'b': 'y';
295+
'c': 'z';
296+
};
297+
declare type TS0<T> = keyof {
298+
[P in keyof T as keyof Record<P, number>]: string;
299+
};
300+
declare type TS1<T> = keyof {
301+
[P in keyof T as Extract<P, 'a' | 'b' | 'c'>]: string;
302+
};
303+
declare type TS2<T> = keyof {
304+
[P in keyof T as P & ('a' | 'b' | 'c')]: string;
305+
};
306+
declare type TS3<T> = keyof {
307+
[P in keyof T as Exclude<P, 'a' | 'b' | 'c'>]: string;
308+
};
309+
declare type TS4<T> = keyof {
310+
[P in keyof T as NameMap[P & keyof NameMap]]: string;
311+
};
312+
declare type TS5<T> = keyof {
313+
[P in keyof T & keyof NameMap as NameMap[P]]: string;
314+
};
315+
declare type TS6<T, U, V> = keyof {
316+
[K in keyof T as V & (K extends U ? K : never)]: string;
317+
};
318+
declare type TN0<T> = keyof {
319+
[P in keyof T as T[P] extends number ? P : never]: string;
320+
};
321+
declare type TN1<T> = keyof {
322+
[P in keyof T as number extends T[P] ? P : never]: string;
323+
};
324+
declare type TN2<T> = keyof {
325+
[P in keyof T as 'a' extends P ? 'x' : 'y']: string;
326+
};
327+
declare type TN3<T> = keyof {
328+
[P in keyof T as Exclude<Exclude<Exclude<P, 'c'>, 'b'>, 'a'>]: string;
329+
};
330+
declare type TN4<T, U> = keyof {
331+
[K in keyof T as (K extends U ? T[K] : never) extends T[K] ? K : never]: string;
332+
};
333+
declare type TN5<T, U> = keyof {
334+
[K in keyof T as keyof {
335+
[P in K as T[P] extends U ? K : never]: true;
336+
}]: string;
337+
};

0 commit comments

Comments
 (0)