Skip to content

Defer mapped type indexed access transformations #18042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 25 additions & 32 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7596,22 +7596,6 @@ namespace ts {
return anyType;
}

function getIndexedAccessForMappedType(type: MappedType, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
if (accessNode) {
// Check if the index type is assignable to 'keyof T' for the object type.
if (!isTypeAssignableTo(indexType, getIndexType(type))) {
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(type));
return unknownType;
}
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) && type.declaration.readonlyToken) {
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(type));
}
}
const mapper = createTypeMapper([getTypeParameterFromMappedType(type)], [indexType]);
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
return instantiateType(getTemplateTypeFromMappedType(type), templateMapper);
}

function isGenericObjectType(type: Type): boolean {
return type.flags & TypeFlags.TypeVariable ? true :
getObjectFlags(type) & ObjectFlags.Mapped ? isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type)) :
Expand All @@ -7637,12 +7621,14 @@ namespace ts {
return false;
}

// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
// access types with default property values as expressed by D.
// Transform an indexed access to a simpler form, if possible. Return the simpler form, or return
// undefined if no transformation is possible.
function getTransformedIndexedAccessType(type: IndexedAccessType): Type {
const objectType = type.objectType;
// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
// access types with default property values as expressed by D.
if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((<IntersectionType>objectType).types, isStringIndexOnlyType)) {
const regularTypes: Type[] = [];
const stringIndexTypes: Type[] = [];
Expand All @@ -7659,20 +7645,23 @@ namespace ts {
getIntersectionType(stringIndexTypes)
]);
}
return undefined;
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
// If the object type is a mapped type { [P in K]: E }, where K is generic, we instantiate E using a mapper
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
// construct the type Box<T[X]>.
if (isGenericMappedType(objectType)) {
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
const mapper = createTypeMapper([getTypeParameterFromMappedType(<MappedType>objectType)], [type.indexType]);
const objectTypeMapper = (<MappedType>objectType).mapper;
const templateMapper = objectTypeMapper ? combineTypeMappers(objectTypeMapper, mapper) : mapper;
return instantiateType(getTemplateTypeFromMappedType(<MappedType>objectType), templateMapper);
}
// Otherwise, if the index type is generic, or if the object type is generic and doesn't originate in an
// expression, we are performing a higher-order index access where we cannot meaningfully access the properties
// of the object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates
// in an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
return undefined;
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
// If the index type is generic, or if the object type is generic and doesn't originate in an expression,
// we are performing a higher-order index access where we cannot meaningfully access the properties of the
// object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates in
// an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
// has always been resolved eagerly using the constraint type of 'this' at the given location.
if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) && isGenericObjectType(objectType)) {
if (objectType.flags & TypeFlags.Any) {
Expand Down Expand Up @@ -9294,7 +9283,7 @@ namespace ts {
else if (target.flags & TypeFlags.IndexedAccess) {
// A type S is related to a type T[K] if S is related to A[K], where K is string-like and
// A is the apparent type of S.
const constraint = getConstraintOfType(<IndexedAccessType>target);
const constraint = getConstraintOfIndexedAccess(<IndexedAccessType>target);
if (constraint) {
if (result = isRelatedTo(source, constraint, reportErrors)) {
errorInfo = saveErrorInfo;
Expand Down Expand Up @@ -9334,7 +9323,7 @@ namespace ts {
else if (source.flags & TypeFlags.IndexedAccess) {
// A type S[K] is related to a type T if A[K] is related to T, where K is string-like and
// A is the apparent type of S.
const constraint = getConstraintOfType(<IndexedAccessType>source);
const constraint = getConstraintOfIndexedAccess(<IndexedAccessType>source);
if (constraint) {
if (result = isRelatedTo(constraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
Expand Down Expand Up @@ -18810,6 +18799,10 @@ namespace ts {
const objectType = (<IndexedAccessType>type).objectType;
const indexType = (<IndexedAccessType>type).indexType;
if (isTypeAssignableTo(indexType, getIndexType(objectType))) {
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
getObjectFlags(objectType) & ObjectFlags.Mapped && (<MappedType>objectType).declaration.readonlyToken) {
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));
}
return type;
}
// Check if we're indexing with a numeric type and if either object or index types
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/isomorphicMappedTypeInference.types
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function boxify<T>(obj: T): Boxified<T> {

result[k] = box(obj[k]);
>result[k] = box(obj[k]) : Box<T[keyof T]>
>result[k] : Box<T[keyof T]>
>result[k] : Boxified<T>[keyof T]
>result : Boxified<T>
>k : keyof T
>box(obj[k]) : Box<T[keyof T]>
Expand Down Expand Up @@ -107,7 +107,7 @@ function unboxify<T>(obj: Boxified<T>): T {
>k : keyof T
>unbox(obj[k]) : T[keyof T]
>unbox : <T>(x: Box<T>) => T
>obj[k] : Box<T[keyof T]>
>obj[k] : Boxified<T>[keyof T]
>obj : Boxified<T>
>k : keyof T
}
Expand All @@ -131,7 +131,7 @@ function assignBoxified<T>(obj: Boxified<T>, values: T) {
obj[k].value = values[k];
>obj[k].value = values[k] : T[keyof T]
>obj[k].value : T[keyof T]
>obj[k] : Box<T[keyof T]>
>obj[k] : Boxified<T>[keyof T]
>obj : Boxified<T>
>k : keyof T
>value : T[keyof T]
Expand Down
24 changes: 12 additions & 12 deletions tests/baselines/reference/keyofAndForIn.types
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ function f1<K extends string, T>(obj: { [P in K]: T }, k: K) {
>obj : { [P in K]: T; }

let x1 = obj[k1];
>x1 : T
>obj[k1] : T
>x1 : { [P in K]: T; }[K]
>obj[k1] : { [P in K]: T; }[K]
>obj : { [P in K]: T; }
>k1 : K
}
Expand All @@ -37,8 +37,8 @@ function f1<K extends string, T>(obj: { [P in K]: T }, k: K) {
>obj : { [P in K]: T; }

let x2 = obj[k2];
>x2 : T
>obj[k2] : T
>x2 : { [P in K]: T; }[K]
>obj[k2] : { [P in K]: T; }[K]
>obj : { [P in K]: T; }
>k2 : K
}
Expand Down Expand Up @@ -70,8 +70,8 @@ function f2<T>(obj: { [P in keyof T]: T[P] }, k: keyof T) {
>obj : { [P in keyof T]: T[P]; }

let x1 = obj[k1];
>x1 : T[keyof T]
>obj[k1] : T[keyof T]
>x1 : { [P in keyof T]: T[P]; }[keyof T]
>obj[k1] : { [P in keyof T]: T[P]; }[keyof T]
>obj : { [P in keyof T]: T[P]; }
>k1 : keyof T
}
Expand All @@ -80,8 +80,8 @@ function f2<T>(obj: { [P in keyof T]: T[P] }, k: keyof T) {
>obj : { [P in keyof T]: T[P]; }

let x2 = obj[k2];
>x2 : T[keyof T]
>obj[k2] : T[keyof T]
>x2 : { [P in keyof T]: T[P]; }[keyof T]
>obj[k2] : { [P in keyof T]: T[P]; }[keyof T]
>obj : { [P in keyof T]: T[P]; }
>k2 : keyof T
}
Expand Down Expand Up @@ -115,8 +115,8 @@ function f3<T, K extends keyof T>(obj: { [P in K]: T[P] }, k: K) {
>obj : { [P in K]: T[P]; }

let x1 = obj[k1];
>x1 : T[K]
>obj[k1] : T[K]
>x1 : { [P in K]: T[P]; }[K]
>obj[k1] : { [P in K]: T[P]; }[K]
>obj : { [P in K]: T[P]; }
>k1 : K
}
Expand All @@ -125,8 +125,8 @@ function f3<T, K extends keyof T>(obj: { [P in K]: T[P] }, k: K) {
>obj : { [P in K]: T[P]; }

let x2 = obj[k2];
>x2 : T[K]
>obj[k2] : T[K]
>x2 : { [P in K]: T[P]; }[K]
>obj[k2] : { [P in K]: T[P]; }[K]
>obj : { [P in K]: T[P]; }
>k2 : K
}
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/keyofAndIndexedAccess.types
Original file line number Diff line number Diff line change
Expand Up @@ -2194,7 +2194,7 @@ class Form<T> {

this.childFormFactories[prop](value)
>this.childFormFactories[prop](value) : Form<T[K]>
>this.childFormFactories[prop] : (v: T[K]) => Form<T[K]>
>this.childFormFactories[prop] : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }[K]
>this.childFormFactories : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }
>this : this
>childFormFactories : { [K in keyof T]: (v: T[K]) => Form<T[K]>; }
Expand Down
49 changes: 49 additions & 0 deletions tests/baselines/reference/mappedTypeIndexedAccess.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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; }'.
Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
Types of property 'value' are incompatible.
Type 'number' is not assignable to type 'string'.
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; }'.
Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
Types of property 'value' are incompatible.
Type 'number' is not assignable to type 'string'.


==== tests/cases/compiler/mappedTypeIndexedAccess.ts (2 errors) ====
// Repro from #15756

type Pairs<T> = {
[TKey in keyof T]: {
key: TKey;
value: T[TKey];
};
};

type Pair<T> = Pairs<T>[keyof T];

type FooBar = {
foo: string;
bar: number;
};

// Error expected here
let pair1: Pair<FooBar> = {
~~~~~
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
!!! error TS2322: Types of property 'value' are incompatible.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
key: "foo",
value: 3
};

// Error expected here
let pair2: Pairs<FooBar>[keyof FooBar] = {
~~~~~
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; } | { key: "bar"; value: number; }'.
!!! error TS2322: Type '{ key: "foo"; value: number; }' is not assignable to type '{ key: "foo"; value: string; }'.
!!! error TS2322: Types of property 'value' are incompatible.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
key: "foo",
value: 3
};

43 changes: 43 additions & 0 deletions tests/baselines/reference/mappedTypeIndexedAccess.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//// [mappedTypeIndexedAccess.ts]
// Repro from #15756

type Pairs<T> = {
[TKey in keyof T]: {
key: TKey;
value: T[TKey];
};
};

type Pair<T> = Pairs<T>[keyof T];

type FooBar = {
foo: string;
bar: number;
};

// Error expected here
let pair1: Pair<FooBar> = {
key: "foo",
value: 3
};

// Error expected here
let pair2: Pairs<FooBar>[keyof FooBar] = {
key: "foo",
value: 3
};


//// [mappedTypeIndexedAccess.js]
"use strict";
// Repro from #15756
// Error expected here
var pair1 = {
key: "foo",
value: 3
};
// Error expected here
var pair2 = {
key: "foo",
value: 3
};
Loading