Skip to content

Commit 2646828

Browse files
committed
Type relations for generic mapped types
1 parent 52ec508 commit 2646828

File tree

3 files changed

+86
-32
lines changed

3 files changed

+86
-32
lines changed

src/compiler/checker.ts

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ namespace ts {
120120
const intersectionTypes = createMap<IntersectionType>();
121121
const stringLiteralTypes = createMap<LiteralType>();
122122
const numericLiteralTypes = createMap<LiteralType>();
123+
const indexedAccessTypes = createMap<IndexedAccessType>();
123124
const evolvingArrayTypes: EvolvingArrayType[] = [];
124125

125126
const unknownSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, "unknown");
@@ -5907,6 +5908,7 @@ namespace ts {
59075908

59085909
function getIndexType(type: Type): Type {
59095910
return type.flags & TypeFlags.TypeParameter ? getIndexTypeForTypeParameter(<TypeParameter>type) :
5911+
getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(<MappedType>type) :
59105912
type.flags & TypeFlags.Any || getIndexInfoOfType(type, IndexKind.String) ? stringOrNumberType :
59115913
getIndexInfoOfType(type, IndexKind.Number) ? getUnionType([numberType, getLiteralTypeFromPropertyNames(type)]) :
59125914
getLiteralTypeFromPropertyNames(type);
@@ -5920,18 +5922,13 @@ namespace ts {
59205922
return links.resolvedType;
59215923
}
59225924

5923-
function createIndexedAccessType(objectType: Type, indexType: TypeParameter) {
5925+
function createIndexedAccessType(objectType: Type, indexType: Type) {
59245926
const type = <IndexedAccessType>createType(TypeFlags.IndexedAccess);
59255927
type.objectType = objectType;
59265928
type.indexType = indexType;
59275929
return type;
59285930
}
59295931

5930-
function getIndexedAccessTypeForTypeParameter(objectType: Type, indexType: TypeParameter) {
5931-
const indexedAccessTypes = indexType.resolvedIndexedAccessTypes || (indexType.resolvedIndexedAccessTypes = []);
5932-
return indexedAccessTypes[objectType.id] || (indexedAccessTypes[objectType.id] = createIndexedAccessType(objectType, indexType));
5933-
}
5934-
59355932
function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode, cacheSymbol: boolean) {
59365933
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? <ElementAccessExpression>accessNode : undefined;
59375934
const propName = indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral | TypeFlags.EnumLiteral) ?
@@ -5995,13 +5992,41 @@ namespace ts {
59955992
return unknownType;
59965993
}
59975994

5995+
function getIndexedAccessForMappedType(type: MappedType, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
5996+
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? <ElementAccessExpression>accessNode : undefined;
5997+
if (accessExpression && isAssignmentTarget(accessExpression) && type.declaration.readonlyToken) {
5998+
error(accessExpression, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(type));
5999+
return unknownType;
6000+
}
6001+
const mapper = createUnaryTypeMapper(getTypeParameterFromMappedType(type), indexType);
6002+
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
6003+
return addOptionality(instantiateType(getTemplateTypeFromMappedType(type), templateMapper), !!type.declaration.questionToken);
6004+
}
6005+
59986006
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
5999-
if (indexType.flags & TypeFlags.TypeParameter) {
6000-
if (accessNode && !isTypeAssignableTo(getConstraintOfTypeParameter(<TypeParameter>indexType) || emptyObjectType, getIndexType(objectType))) {
6001-
error(accessNode, Diagnostics.Type_0_is_not_constrained_to_keyof_1, typeToString(indexType), typeToString(objectType));
6002-
return unknownType;
6007+
if (indexType.flags & TypeFlags.TypeParameter ||
6008+
objectType.flags & TypeFlags.TypeParameter && indexType.flags & TypeFlags.Index ||
6009+
isGenericMappedType(objectType)) {
6010+
// If either the object type or the index type are type parameters, or if the object type is a mapped
6011+
// type with a generic constraint, we are performing a higher-order index access where we cannot
6012+
// meaningfully access the properties of the object type. In those cases, we first check that the
6013+
// index type is assignable to 'keyof T' for the object type.
6014+
if (accessNode) {
6015+
const keyType = indexType.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>indexType) || emptyObjectType : indexType;
6016+
if (!isTypeAssignableTo(keyType, getIndexType(objectType))) {
6017+
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
6018+
return unknownType;
6019+
}
60036020
}
6004-
return getIndexedAccessTypeForTypeParameter(objectType, <TypeParameter>indexType);
6021+
// If the object type is a mapped type { [P in K]: E }, we instantiate E using a mapper that substitutes
6022+
// the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the
6023+
// type Box<T[X]>.
6024+
if (isGenericMappedType(objectType)) {
6025+
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
6026+
}
6027+
// Otherwise we defer the operation by creating an indexed access type.
6028+
const id = objectType.id + "," + indexType.id;
6029+
return indexedAccessTypes[id] || (indexedAccessTypes[id] = createIndexedAccessType(objectType, indexType));
60056030
}
60066031
const apparentType = getApparentType(objectType);
60076032
if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Primitive)) {
@@ -7153,12 +7178,24 @@ namespace ts {
71537178
}
71547179

71557180
if (target.flags & TypeFlags.TypeParameter) {
7156-
// Given a type parameter K with a constraint keyof T, a type S is
7157-
// assignable to K if S is assignable to keyof T.
7158-
const constraint = getConstraintOfTypeParameter(<TypeParameter>target);
7159-
if (constraint && constraint.flags & TypeFlags.Index) {
7160-
if (result = isRelatedTo(source, constraint, reportErrors)) {
7161-
return result;
7181+
// A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P].
7182+
if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>source) === getIndexType(target)) {
7183+
if (!(<MappedType>source).declaration.questionToken) {
7184+
const templateType = getTemplateTypeFromMappedType(<MappedType>source);
7185+
const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(<MappedType>source));
7186+
if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) {
7187+
return result;
7188+
}
7189+
}
7190+
}
7191+
else {
7192+
// Given a type parameter K with a constraint keyof T, a type S is
7193+
// assignable to K if S is assignable to keyof T.
7194+
const constraint = getConstraintOfTypeParameter(<TypeParameter>target);
7195+
if (constraint && constraint.flags & TypeFlags.Index) {
7196+
if (result = isRelatedTo(source, constraint, reportErrors)) {
7197+
return result;
7198+
}
71627199
}
71637200
}
71647201
}
@@ -7178,22 +7215,41 @@ namespace ts {
71787215
}
71797216
}
71807217
}
7218+
else if (target.flags & TypeFlags.IndexedAccess) {
7219+
// if we have indexed access types with identical index types, see if relationship holds for
7220+
// the two object types.
7221+
if (source.flags & TypeFlags.IndexedAccess && (<IndexedAccessType>source).indexType === (<IndexedAccessType>target).indexType) {
7222+
if (result = isRelatedTo((<IndexedAccessType>source).objectType, (<IndexedAccessType>target).objectType, reportErrors)) {
7223+
return result;
7224+
}
7225+
}
7226+
}
71817227

71827228
if (source.flags & TypeFlags.TypeParameter) {
7183-
let constraint = getConstraintOfTypeParameter(<TypeParameter>source);
7184-
7185-
if (!constraint || constraint.flags & TypeFlags.Any) {
7186-
constraint = emptyObjectType;
7229+
// A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X.
7230+
if (getObjectFlags(target) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>target) === getIndexType(source)) {
7231+
const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(<MappedType>target));
7232+
const templateType = getTemplateTypeFromMappedType(<MappedType>target);
7233+
if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) {
7234+
return result;
7235+
}
71877236
}
7237+
else {
7238+
let constraint = getConstraintOfTypeParameter(<TypeParameter>source);
7239+
7240+
if (!constraint || constraint.flags & TypeFlags.Any) {
7241+
constraint = emptyObjectType;
7242+
}
71887243

7189-
// The constraint may need to be further instantiated with its 'this' type.
7190-
constraint = getTypeWithThisArgument(constraint, source);
7244+
// The constraint may need to be further instantiated with its 'this' type.
7245+
constraint = getTypeWithThisArgument(constraint, source);
71917246

7192-
// Report constraint errors only if the constraint is not the empty object type
7193-
const reportConstraintErrors = reportErrors && constraint !== emptyObjectType;
7194-
if (result = isRelatedTo(constraint, target, reportConstraintErrors)) {
7195-
errorInfo = saveErrorInfo;
7196-
return result;
7247+
// Report constraint errors only if the constraint is not the empty object type
7248+
const reportConstraintErrors = reportErrors && constraint !== emptyObjectType;
7249+
if (result = isRelatedTo(constraint, target, reportConstraintErrors)) {
7250+
errorInfo = saveErrorInfo;
7251+
return result;
7252+
}
71977253
}
71987254
}
71997255
else {

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,7 @@
17311731
"category": "Error",
17321732
"code": 2535
17331733
},
1734-
"Type '{0}' is not constrained to 'keyof {1}'.": {
1734+
"Type '{0}' cannot be used to index type '{1}'.": {
17351735
"category": "Error",
17361736
"code": 2536
17371737
},

src/compiler/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2974,8 +2974,6 @@ namespace ts {
29742974
/* @internal */
29752975
resolvedIndexType: IndexType;
29762976
/* @internal */
2977-
resolvedIndexedAccessTypes: IndexedAccessType[];
2978-
/* @internal */
29792977
isThisType?: boolean;
29802978
}
29812979

@@ -2985,7 +2983,7 @@ namespace ts {
29852983

29862984
export interface IndexedAccessType extends Type {
29872985
objectType: Type;
2988-
indexType: TypeParameter;
2986+
indexType: Type;
29892987
}
29902988

29912989
export const enum SignatureKind {

0 commit comments

Comments
 (0)