Skip to content

Commit 555a742

Browse files
authored
Merge pull request #18042 from Microsoft/fixMappedTypeIndexedAccess
Defer mapped type indexed access transformations
2 parents fe1242c + e79d75a commit 555a742

10 files changed

+300
-126
lines changed

src/compiler/checker.ts

+25-32
Original file line numberDiff line numberDiff line change
@@ -7612,22 +7612,6 @@ namespace ts {
76127612
return anyType;
76137613
}
76147614

7615-
function getIndexedAccessForMappedType(type: MappedType, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
7616-
if (accessNode) {
7617-
// Check if the index type is assignable to 'keyof T' for the object type.
7618-
if (!isTypeAssignableTo(indexType, getIndexType(type))) {
7619-
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(type));
7620-
return unknownType;
7621-
}
7622-
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) && type.declaration.readonlyToken) {
7623-
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(type));
7624-
}
7625-
}
7626-
const mapper = createTypeMapper([getTypeParameterFromMappedType(type)], [indexType]);
7627-
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
7628-
return instantiateType(getTemplateTypeFromMappedType(type), templateMapper);
7629-
}
7630-
76317615
function isGenericObjectType(type: Type): boolean {
76327616
return type.flags & TypeFlags.TypeVariable ? true :
76337617
getObjectFlags(type) & ObjectFlags.Mapped ? isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type)) :
@@ -7653,12 +7637,14 @@ namespace ts {
76537637
return false;
76547638
}
76557639

7656-
// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
7657-
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
7658-
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
7659-
// access types with default property values as expressed by D.
7640+
// Transform an indexed access to a simpler form, if possible. Return the simpler form, or return
7641+
// undefined if no transformation is possible.
76607642
function getTransformedIndexedAccessType(type: IndexedAccessType): Type {
76617643
const objectType = type.objectType;
7644+
// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
7645+
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
7646+
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
7647+
// access types with default property values as expressed by D.
76627648
if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((<IntersectionType>objectType).types, isStringIndexOnlyType)) {
76637649
const regularTypes: Type[] = [];
76647650
const stringIndexTypes: Type[] = [];
@@ -7675,20 +7661,23 @@ namespace ts {
76757661
getIntersectionType(stringIndexTypes)
76767662
]);
76777663
}
7678-
return undefined;
7679-
}
7680-
7681-
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
7682-
// If the object type is a mapped type { [P in K]: E }, where K is generic, we instantiate E using a mapper
7664+
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
76837665
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
76847666
// construct the type Box<T[X]>.
76857667
if (isGenericMappedType(objectType)) {
7686-
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
7668+
const mapper = createTypeMapper([getTypeParameterFromMappedType(<MappedType>objectType)], [type.indexType]);
7669+
const objectTypeMapper = (<MappedType>objectType).mapper;
7670+
const templateMapper = objectTypeMapper ? combineTypeMappers(objectTypeMapper, mapper) : mapper;
7671+
return instantiateType(getTemplateTypeFromMappedType(<MappedType>objectType), templateMapper);
76877672
}
7688-
// Otherwise, if the index type is generic, or if the object type is generic and doesn't originate in an
7689-
// expression, we are performing a higher-order index access where we cannot meaningfully access the properties
7690-
// of the object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates
7691-
// in an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
7673+
return undefined;
7674+
}
7675+
7676+
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
7677+
// If the index type is generic, or if the object type is generic and doesn't originate in an expression,
7678+
// we are performing a higher-order index access where we cannot meaningfully access the properties of the
7679+
// object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates in
7680+
// an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
76927681
// has always been resolved eagerly using the constraint type of 'this' at the given location.
76937682
if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) && isGenericObjectType(objectType)) {
76947683
if (objectType.flags & TypeFlags.Any) {
@@ -9310,7 +9299,7 @@ namespace ts {
93109299
else if (target.flags & TypeFlags.IndexedAccess) {
93119300
// A type S is related to a type T[K] if S is related to A[K], where K is string-like and
93129301
// A is the apparent type of S.
9313-
const constraint = getConstraintOfType(<IndexedAccessType>target);
9302+
const constraint = getConstraintOfIndexedAccess(<IndexedAccessType>target);
93149303
if (constraint) {
93159304
if (result = isRelatedTo(source, constraint, reportErrors)) {
93169305
errorInfo = saveErrorInfo;
@@ -9350,7 +9339,7 @@ namespace ts {
93509339
else if (source.flags & TypeFlags.IndexedAccess) {
93519340
// A type S[K] is related to a type T if A[K] is related to T, where K is string-like and
93529341
// A is the apparent type of S.
9353-
const constraint = getConstraintOfType(<IndexedAccessType>source);
9342+
const constraint = getConstraintOfIndexedAccess(<IndexedAccessType>source);
93549343
if (constraint) {
93559344
if (result = isRelatedTo(constraint, target, reportErrors)) {
93569345
errorInfo = saveErrorInfo;
@@ -18839,6 +18828,10 @@ namespace ts {
1883918828
const objectType = (<IndexedAccessType>type).objectType;
1884018829
const indexType = (<IndexedAccessType>type).indexType;
1884118830
if (isTypeAssignableTo(indexType, getIndexType(objectType))) {
18831+
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
18832+
getObjectFlags(objectType) & ObjectFlags.Mapped && (<MappedType>objectType).declaration.readonlyToken) {
18833+
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));
18834+
}
1884218835
return type;
1884318836
}
1884418837
// Check if we're indexing with a numeric type and if either object or index types

tests/baselines/reference/isomorphicMappedTypeInference.types

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function boxify<T>(obj: T): Boxified<T> {
6969

7070
result[k] = box(obj[k]);
7171
>result[k] = box(obj[k]) : Box<T[keyof T]>
72-
>result[k] : Box<T[keyof T]>
72+
>result[k] : Boxified<T>[keyof T]
7373
>result : Boxified<T>
7474
>k : keyof T
7575
>box(obj[k]) : Box<T[keyof T]>
@@ -107,7 +107,7 @@ function unboxify<T>(obj: Boxified<T>): T {
107107
>k : keyof T
108108
>unbox(obj[k]) : T[keyof T]
109109
>unbox : <T>(x: Box<T>) => T
110-
>obj[k] : Box<T[keyof T]>
110+
>obj[k] : Boxified<T>[keyof T]
111111
>obj : Boxified<T>
112112
>k : keyof T
113113
}
@@ -131,7 +131,7 @@ function assignBoxified<T>(obj: Boxified<T>, values: T) {
131131
obj[k].value = values[k];
132132
>obj[k].value = values[k] : T[keyof T]
133133
>obj[k].value : T[keyof T]
134-
>obj[k] : Box<T[keyof T]>
134+
>obj[k] : Boxified<T>[keyof T]
135135
>obj : Boxified<T>
136136
>k : keyof T
137137
>value : T[keyof T]

tests/baselines/reference/keyofAndForIn.types

+12-12
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ function f1<K extends string, T>(obj: { [P in K]: T }, k: K) {
2727
>obj : { [P in K]: T; }
2828

2929
let x1 = obj[k1];
30-
>x1 : T
31-
>obj[k1] : T
30+
>x1 : { [P in K]: T; }[K]
31+
>obj[k1] : { [P in K]: T; }[K]
3232
>obj : { [P in K]: T; }
3333
>k1 : K
3434
}
@@ -37,8 +37,8 @@ function f1<K extends string, T>(obj: { [P in K]: T }, k: K) {
3737
>obj : { [P in K]: T; }
3838

3939
let x2 = obj[k2];
40-
>x2 : T
41-
>obj[k2] : T
40+
>x2 : { [P in K]: T; }[K]
41+
>obj[k2] : { [P in K]: T; }[K]
4242
>obj : { [P in K]: T; }
4343
>k2 : K
4444
}
@@ -70,8 +70,8 @@ function f2<T>(obj: { [P in keyof T]: T[P] }, k: keyof T) {
7070
>obj : { [P in keyof T]: T[P]; }
7171

7272
let x1 = obj[k1];
73-
>x1 : T[keyof T]
74-
>obj[k1] : T[keyof T]
73+
>x1 : { [P in keyof T]: T[P]; }[keyof T]
74+
>obj[k1] : { [P in keyof T]: T[P]; }[keyof T]
7575
>obj : { [P in keyof T]: T[P]; }
7676
>k1 : keyof T
7777
}
@@ -80,8 +80,8 @@ function f2<T>(obj: { [P in keyof T]: T[P] }, k: keyof T) {
8080
>obj : { [P in keyof T]: T[P]; }
8181

8282
let x2 = obj[k2];
83-
>x2 : T[keyof T]
84-
>obj[k2] : T[keyof T]
83+
>x2 : { [P in keyof T]: T[P]; }[keyof T]
84+
>obj[k2] : { [P in keyof T]: T[P]; }[keyof T]
8585
>obj : { [P in keyof T]: T[P]; }
8686
>k2 : keyof T
8787
}
@@ -115,8 +115,8 @@ function f3<T, K extends keyof T>(obj: { [P in K]: T[P] }, k: K) {
115115
>obj : { [P in K]: T[P]; }
116116

117117
let x1 = obj[k1];
118-
>x1 : T[K]
119-
>obj[k1] : T[K]
118+
>x1 : { [P in K]: T[P]; }[K]
119+
>obj[k1] : { [P in K]: T[P]; }[K]
120120
>obj : { [P in K]: T[P]; }
121121
>k1 : K
122122
}
@@ -125,8 +125,8 @@ function f3<T, K extends keyof T>(obj: { [P in K]: T[P] }, k: K) {
125125
>obj : { [P in K]: T[P]; }
126126

127127
let x2 = obj[k2];
128-
>x2 : T[K]
129-
>obj[k2] : T[K]
128+
>x2 : { [P in K]: T[P]; }[K]
129+
>obj[k2] : { [P in K]: T[P]; }[K]
130130
>obj : { [P in K]: T[P]; }
131131
>k2 : K
132132
}

tests/baselines/reference/keyofAndIndexedAccess.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -2194,7 +2194,7 @@ class Form<T> {
21942194

21952195
this.childFormFactories[prop](value)
21962196
>this.childFormFactories[prop](value) : Form<T[K]>
2197-
>this.childFormFactories[prop] : (v: T[K]) => Form<T[K]>
2197+
>this.childFormFactories[prop] : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }[K]
21982198
>this.childFormFactories : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }
21992199
>this : this
22002200
>childFormFactories : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
tests/cases/compiler/mappedTypeIndexedAccess.ts(18,5): error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
2+
Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
3+
Types of property 'value' are incompatible.
4+
Type 'number' is not assignable to type 'string'.
5+
tests/cases/compiler/mappedTypeIndexedAccess.ts(24,5): error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
6+
Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
7+
Types of property 'value' are incompatible.
8+
Type 'number' is not assignable to type 'string'.
9+
10+
11+
==== tests/cases/compiler/mappedTypeIndexedAccess.ts (2 errors) ====
12+
// Repro from #15756
13+
14+
type Pairs<T> = {
15+
[TKey in keyof T]: {
16+
key: TKey;
17+
value: T[TKey];
18+
};
19+
};
20+
21+
type Pair<T> = Pairs<T>[keyof T];
22+
23+
type FooBar = {
24+
foo: string;
25+
bar: number;
26+
};
27+
28+
// Error expected here
29+
let pair1: Pair<FooBar> = {
30+
~~~~~
31+
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
32+
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
33+
!!! error TS2322: Types of property 'value' are incompatible.
34+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
35+
key: "foo",
36+
value: 3
37+
};
38+
39+
// Error expected here
40+
let pair2: Pairs<FooBar>[keyof FooBar] = {
41+
~~~~~
42+
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
43+
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
44+
!!! error TS2322: Types of property 'value' are incompatible.
45+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
46+
key: "foo",
47+
value: 3
48+
};
49+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//// [mappedTypeIndexedAccess.ts]
2+
// Repro from #15756
3+
4+
type Pairs<T> = {
5+
[TKey in keyof T]: {
6+
key: TKey;
7+
value: T[TKey];
8+
};
9+
};
10+
11+
type Pair<T> = Pairs<T>[keyof T];
12+
13+
type FooBar = {
14+
foo: string;
15+
bar: number;
16+
};
17+
18+
// Error expected here
19+
let pair1: Pair<FooBar> = {
20+
key: "foo",
21+
value: 3
22+
};
23+
24+
// Error expected here
25+
let pair2: Pairs<FooBar>[keyof FooBar] = {
26+
key: "foo",
27+
value: 3
28+
};
29+
30+
31+
//// [mappedTypeIndexedAccess.js]
32+
"use strict";
33+
// Repro from #15756
34+
// Error expected here
35+
var pair1 = {
36+
key: "foo",
37+
value: 3
38+
};
39+
// Error expected here
40+
var pair2 = {
41+
key: "foo",
42+
value: 3
43+
};

0 commit comments

Comments
 (0)