Skip to content

Commit e128add

Browse files
authored
Merge pull request #12447 from Microsoft/mappedTypesAndUnions
Isomorphic mapped types and unions
2 parents 72cee3e + 43e383f commit e128add

File tree

13 files changed

+880
-194
lines changed

13 files changed

+880
-194
lines changed

src/compiler/checker.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6495,7 +6495,36 @@ namespace ts {
64956495
return result;
64966496
}
64976497

6498-
function instantiateMappedType(type: MappedType, mapper: TypeMapper): MappedType {
6498+
function instantiateMappedType(type: MappedType, mapper: TypeMapper): Type {
6499+
// Check if we have an isomorphic mapped type, i.e. a type of the form { [P in keyof T]: X } for some
6500+
// type parameter T. If so, the mapped type is distributive over a union type and when T is instantiated
6501+
// to a union type A | B, we produce { [P in keyof A]: X } | { [P in keyof B]: X }. Furthermore, for
6502+
// isomorphic mapped types we leave primitive types alone. For example, when T is instantiated to a
6503+
// union type A | undefined, we produce { [P in keyof A]: X } | undefined.
6504+
const constraintType = getConstraintTypeFromMappedType(type);
6505+
if (constraintType.flags & TypeFlags.Index) {
6506+
const typeParameter = (<IndexType>constraintType).type;
6507+
const mappedTypeParameter = mapper(typeParameter);
6508+
if (typeParameter !== mappedTypeParameter) {
6509+
return mapType(mappedTypeParameter, t => {
6510+
if (isMappableType(t)) {
6511+
const replacementMapper = createUnaryTypeMapper(typeParameter, t);
6512+
const combinedMapper = mapper.mappedTypes && mapper.mappedTypes.length === 1 ? replacementMapper : combineTypeMappers(replacementMapper, mapper);
6513+
combinedMapper.mappedTypes = mapper.mappedTypes;
6514+
return instantiateMappedObjectType(type, combinedMapper);
6515+
}
6516+
return t;
6517+
});
6518+
}
6519+
}
6520+
return instantiateMappedObjectType(type, mapper);
6521+
}
6522+
6523+
function isMappableType(type: Type) {
6524+
return type.flags & (TypeFlags.TypeParameter | TypeFlags.Object | TypeFlags.Intersection | TypeFlags.IndexedAccess);
6525+
}
6526+
6527+
function instantiateMappedObjectType(type: MappedType, mapper: TypeMapper): Type {
64996528
const result = <MappedType>createObjectType(ObjectFlags.Mapped | ObjectFlags.Instantiated, type.symbol);
65006529
result.declaration = type.declaration;
65016530
result.mapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
@@ -7537,25 +7566,30 @@ namespace ts {
75377566

75387567
// A type [P in S]: X is related to a type [P in T]: Y if T is related to S and X is related to Y.
75397568
function mappedTypeRelatedTo(source: Type, target: Type, reportErrors: boolean): Ternary {
7540-
if (isGenericMappedType(source) && isGenericMappedType(target)) {
7541-
let result: Ternary;
7542-
if (relation === identityRelation) {
7543-
const readonlyMatches = !(<MappedType>source).declaration.readonlyToken === !(<MappedType>target).declaration.readonlyToken;
7544-
const optionalMatches = !(<MappedType>source).declaration.questionToken === !(<MappedType>target).declaration.questionToken;
7545-
if (readonlyMatches && optionalMatches) {
7546-
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
7547-
return result & isRelatedTo(getErasedTemplateTypeFromMappedType(<MappedType>source), getErasedTemplateTypeFromMappedType(<MappedType>target), reportErrors);
7569+
if (isGenericMappedType(target)) {
7570+
if (isGenericMappedType(source)) {
7571+
let result: Ternary;
7572+
if (relation === identityRelation) {
7573+
const readonlyMatches = !(<MappedType>source).declaration.readonlyToken === !(<MappedType>target).declaration.readonlyToken;
7574+
const optionalMatches = !(<MappedType>source).declaration.questionToken === !(<MappedType>target).declaration.questionToken;
7575+
if (readonlyMatches && optionalMatches) {
7576+
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
7577+
return result & isRelatedTo(getErasedTemplateTypeFromMappedType(<MappedType>source), getErasedTemplateTypeFromMappedType(<MappedType>target), reportErrors);
7578+
}
75487579
}
75497580
}
7550-
}
7551-
else {
7552-
if (relation === comparableRelation || !(<MappedType>source).declaration.questionToken || (<MappedType>target).declaration.questionToken) {
7553-
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
7554-
return result & isRelatedTo(getTemplateTypeFromMappedType(<MappedType>source), getTemplateTypeFromMappedType(<MappedType>target), reportErrors);
7581+
else {
7582+
if (relation === comparableRelation || !(<MappedType>source).declaration.questionToken || (<MappedType>target).declaration.questionToken) {
7583+
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
7584+
return result & isRelatedTo(getTemplateTypeFromMappedType(<MappedType>source), getTemplateTypeFromMappedType(<MappedType>target), reportErrors);
7585+
}
75557586
}
75567587
}
75577588
}
75587589
}
7590+
else if (relation !== identityRelation && isEmptyObjectType(resolveStructuredTypeMembers(<ObjectType>target))) {
7591+
return Ternary.True;
7592+
}
75597593
return Ternary.False;
75607594
}
75617595

tests/baselines/reference/mappedTypes1.js

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ type T47 = { [P in string | "a" | "b" | "0" | "1"]: void };
3333
declare function f1<T1>(): { [P in keyof T1]: void };
3434
declare function f2<T1 extends string>(): { [P in keyof T1]: void };
3535
declare function f3<T1 extends number>(): { [P in keyof T1]: void };
36+
declare function f4<T1 extends Number>(): { [P in keyof T1]: void };
3637

3738
let x1 = f1();
3839
let x2 = f2();
39-
let x3 = f3();
40+
let x3 = f3();
41+
let x4 = f4();
4042

4143
//// [mappedTypes1.js]
4244
var x1 = f1();
4345
var x2 = f2();
4446
var x3 = f3();
47+
var x4 = f4();
4548

4649

4750
//// [mappedTypes1.d.ts]
@@ -128,31 +131,13 @@ declare function f2<T1 extends string>(): {
128131
declare function f3<T1 extends number>(): {
129132
[P in keyof T1]: void;
130133
};
131-
declare let x1: {};
132-
declare let x2: {
133-
toString: void;
134-
charAt: void;
135-
charCodeAt: void;
136-
concat: void;
137-
indexOf: void;
138-
lastIndexOf: void;
139-
localeCompare: void;
140-
match: void;
141-
replace: void;
142-
search: void;
143-
slice: void;
144-
split: void;
145-
substring: void;
146-
toLowerCase: void;
147-
toLocaleLowerCase: void;
148-
toUpperCase: void;
149-
toLocaleUpperCase: void;
150-
trim: void;
151-
length: void;
152-
substr: void;
153-
valueOf: void;
134+
declare function f4<T1 extends Number>(): {
135+
[P in keyof T1]: void;
154136
};
155-
declare let x3: {
137+
declare let x1: {};
138+
declare let x2: string;
139+
declare let x3: number;
140+
declare let x4: {
156141
toString: void;
157142
valueOf: void;
158143
toFixed: void;

tests/baselines/reference/mappedTypes1.symbols

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,26 @@ declare function f3<T1 extends number>(): { [P in keyof T1]: void };
140140
>P : Symbol(P, Decl(mappedTypes1.ts, 33, 45))
141141
>T1 : Symbol(T1, Decl(mappedTypes1.ts, 33, 20))
142142

143+
declare function f4<T1 extends Number>(): { [P in keyof T1]: void };
144+
>f4 : Symbol(f4, Decl(mappedTypes1.ts, 33, 68))
145+
>T1 : Symbol(T1, Decl(mappedTypes1.ts, 34, 20))
146+
>Number : Symbol(Number, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
147+
>P : Symbol(P, Decl(mappedTypes1.ts, 34, 45))
148+
>T1 : Symbol(T1, Decl(mappedTypes1.ts, 34, 20))
149+
143150
let x1 = f1();
144-
>x1 : Symbol(x1, Decl(mappedTypes1.ts, 35, 3))
151+
>x1 : Symbol(x1, Decl(mappedTypes1.ts, 36, 3))
145152
>f1 : Symbol(f1, Decl(mappedTypes1.ts, 29, 59))
146153

147154
let x2 = f2();
148-
>x2 : Symbol(x2, Decl(mappedTypes1.ts, 36, 3))
155+
>x2 : Symbol(x2, Decl(mappedTypes1.ts, 37, 3))
149156
>f2 : Symbol(f2, Decl(mappedTypes1.ts, 31, 53))
150157

151158
let x3 = f3();
152-
>x3 : Symbol(x3, Decl(mappedTypes1.ts, 37, 3))
159+
>x3 : Symbol(x3, Decl(mappedTypes1.ts, 38, 3))
153160
>f3 : Symbol(f3, Decl(mappedTypes1.ts, 32, 68))
154161

162+
let x4 = f4();
163+
>x4 : Symbol(x4, Decl(mappedTypes1.ts, 39, 3))
164+
>f4 : Symbol(f4, Decl(mappedTypes1.ts, 33, 68))
165+

tests/baselines/reference/mappedTypes1.types

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,30 @@ declare function f3<T1 extends number>(): { [P in keyof T1]: void };
142142
>P : P
143143
>T1 : T1
144144

145+
declare function f4<T1 extends Number>(): { [P in keyof T1]: void };
146+
>f4 : <T1 extends Number>() => { [P in keyof T1]: void; }
147+
>T1 : T1
148+
>Number : Number
149+
>P : P
150+
>T1 : T1
151+
145152
let x1 = f1();
146153
>x1 : {}
147154
>f1() : {}
148155
>f1 : <T1>() => { [P in keyof T1]: void; }
149156

150157
let x2 = f2();
151-
>x2 : { toString: void; charAt: void; charCodeAt: void; concat: void; indexOf: void; lastIndexOf: void; localeCompare: void; match: void; replace: void; search: void; slice: void; split: void; substring: void; toLowerCase: void; toLocaleLowerCase: void; toUpperCase: void; toLocaleUpperCase: void; trim: void; length: void; substr: void; valueOf: void; }
152-
>f2() : { toString: void; charAt: void; charCodeAt: void; concat: void; indexOf: void; lastIndexOf: void; localeCompare: void; match: void; replace: void; search: void; slice: void; split: void; substring: void; toLowerCase: void; toLocaleLowerCase: void; toUpperCase: void; toLocaleUpperCase: void; trim: void; length: void; substr: void; valueOf: void; }
158+
>x2 : string
159+
>f2() : string
153160
>f2 : <T1 extends string>() => { [P in keyof T1]: void; }
154161

155162
let x3 = f3();
156-
>x3 : { toString: void; valueOf: void; toFixed: void; toExponential: void; toPrecision: void; toLocaleString: void; }
157-
>f3() : { toString: void; valueOf: void; toFixed: void; toExponential: void; toPrecision: void; toLocaleString: void; }
163+
>x3 : number
164+
>f3() : number
158165
>f3 : <T1 extends number>() => { [P in keyof T1]: void; }
159166

167+
let x4 = f4();
168+
>x4 : { toString: void; valueOf: void; toFixed: void; toExponential: void; toPrecision: void; toLocaleString: void; }
169+
>f4() : { toString: void; valueOf: void; toFixed: void; toExponential: void; toPrecision: void; toLocaleString: void; }
170+
>f4 : <T1 extends Number>() => { [P in keyof T1]: void; }
171+

tests/baselines/reference/mappedTypes2.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,30 @@ declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
3030
declare function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>;
3131
declare function proxify<T>(obj: T): Proxify<T>;
3232

33+
interface Point {
34+
x: number;
35+
y: number;
36+
}
37+
3338
interface Shape {
3439
name: string;
3540
width: number;
3641
height: number;
37-
visible: boolean;
42+
location: Point;
3843
}
3944

4045
interface PartialShape {
4146
name?: string;
4247
width?: number;
4348
height?: number;
44-
visible?: boolean;
49+
location?: Point;
4550
}
4651

4752
interface ReadonlyShape {
4853
readonly name: string;
4954
readonly width: number;
5055
readonly height: number;
51-
readonly visible: boolean;
56+
readonly location: Point;
5257
}
5358

5459
function f0(s1: Shape, s2: Shape) {
@@ -69,7 +74,7 @@ function f2(shape: Shape) {
6974
}
7075

7176
function f3(shape: Shape) {
72-
const x = pick(shape, "name", "visible"); // { name: string, visible: boolean }
77+
const x = pick(shape, "name", "location"); // { name: string, location: Point }
7378
}
7479

7580
function f4() {
@@ -80,13 +85,13 @@ function f4() {
8085
function f5(shape: Shape) {
8186
const p = proxify(shape);
8287
let name = p.name.get();
83-
p.visible.set(false);
88+
p.width.set(42);
8489
}
8590

8691
function f6(shape: DeepReadonly<Shape>) {
87-
let name = shape.name; // DeepReadonly<string>
88-
let length = name.length; // DeepReadonly<number>
89-
let toString = length.toString; // DeepReadonly<(radix?: number) => string>
92+
let name = shape.name; // string
93+
let location = shape.location; // DeepReadonly<Point>
94+
let x = location.x; // number
9095
}
9196

9297
//// [mappedTypes2.js]
@@ -115,7 +120,7 @@ function f2(shape) {
115120
var partial = {};
116121
}
117122
function f3(shape) {
118-
var x = pick(shape, "name", "visible"); // { name: string, visible: boolean }
123+
var x = pick(shape, "name", "location"); // { name: string, location: Point }
119124
}
120125
function f4() {
121126
var rec = { foo: "hello", bar: "world", baz: "bye" };
@@ -124,12 +129,12 @@ function f4() {
124129
function f5(shape) {
125130
var p = proxify(shape);
126131
var name = p.name.get();
127-
p.visible.set(false);
132+
p.width.set(42);
128133
}
129134
function f6(shape) {
130-
var name = shape.name; // DeepReadonly<string>
131-
var length = name.length; // DeepReadonly<number>
132-
var toString = length.toString; // DeepReadonly<(radix?: number) => string>
135+
var name = shape.name; // string
136+
var location = shape.location; // DeepReadonly<Point>
137+
var x = location.x; // number
133138
}
134139

135140

@@ -150,23 +155,27 @@ declare function freeze<T>(obj: T): Readonly<T>;
150155
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
151156
declare function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>;
152157
declare function proxify<T>(obj: T): Proxify<T>;
158+
interface Point {
159+
x: number;
160+
y: number;
161+
}
153162
interface Shape {
154163
name: string;
155164
width: number;
156165
height: number;
157-
visible: boolean;
166+
location: Point;
158167
}
159168
interface PartialShape {
160169
name?: string;
161170
width?: number;
162171
height?: number;
163-
visible?: boolean;
172+
location?: Point;
164173
}
165174
interface ReadonlyShape {
166175
readonly name: string;
167176
readonly width: number;
168177
readonly height: number;
169-
readonly visible: boolean;
178+
readonly location: Point;
170179
}
171180
declare function f0(s1: Shape, s2: Shape): void;
172181
declare function f1(shape: Shape): void;

0 commit comments

Comments
 (0)