From 398244177df8596be86c6dac623cd8057fd3536f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 17 Nov 2016 20:18:00 -0800 Subject: [PATCH] Type relations for generic mapped types --- src/compiler/checker.ts | 112 ++++++++++++++++++++------- src/compiler/diagnosticMessages.json | 2 +- src/compiler/types.ts | 4 +- 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3d25fc8bbb..5ea185556f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -120,6 +120,7 @@ namespace ts { const intersectionTypes = createMap(); const stringLiteralTypes = createMap(); const numericLiteralTypes = createMap(); + const indexedAccessTypes = createMap(); const evolvingArrayTypes: EvolvingArrayType[] = []; const unknownSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, "unknown"); @@ -5907,6 +5908,7 @@ namespace ts { function getIndexType(type: Type): Type { return type.flags & TypeFlags.TypeParameter ? getIndexTypeForTypeParameter(type) : + getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(type) : type.flags & TypeFlags.Any || getIndexInfoOfType(type, IndexKind.String) ? stringOrNumberType : getIndexInfoOfType(type, IndexKind.Number) ? getUnionType([numberType, getLiteralTypeFromPropertyNames(type)]) : getLiteralTypeFromPropertyNames(type); @@ -5920,18 +5922,13 @@ namespace ts { return links.resolvedType; } - function createIndexedAccessType(objectType: Type, indexType: TypeParameter) { + function createIndexedAccessType(objectType: Type, indexType: Type) { const type = createType(TypeFlags.IndexedAccess); type.objectType = objectType; type.indexType = indexType; return type; } - function getIndexedAccessTypeForTypeParameter(objectType: Type, indexType: TypeParameter) { - const indexedAccessTypes = indexType.resolvedIndexedAccessTypes || (indexType.resolvedIndexedAccessTypes = []); - return indexedAccessTypes[objectType.id] || (indexedAccessTypes[objectType.id] = createIndexedAccessType(objectType, indexType)); - } - function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode, cacheSymbol: boolean) { const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; const propName = indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral | TypeFlags.EnumLiteral) ? @@ -5995,13 +5992,41 @@ namespace ts { return unknownType; } + function getIndexedAccessForMappedType(type: MappedType, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) { + const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; + if (accessExpression && isAssignmentTarget(accessExpression) && type.declaration.readonlyToken) { + error(accessExpression, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(type)); + return unknownType; + } + const mapper = createUnaryTypeMapper(getTypeParameterFromMappedType(type), indexType); + const templateMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper; + return addOptionality(instantiateType(getTemplateTypeFromMappedType(type), templateMapper), !!type.declaration.questionToken); + } + function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) { - if (indexType.flags & TypeFlags.TypeParameter) { - if (accessNode && !isTypeAssignableTo(getConstraintOfTypeParameter(indexType) || emptyObjectType, getIndexType(objectType))) { - error(accessNode, Diagnostics.Type_0_is_not_constrained_to_keyof_1, typeToString(indexType), typeToString(objectType)); - return unknownType; + if (indexType.flags & TypeFlags.TypeParameter || + objectType.flags & TypeFlags.TypeParameter && indexType.flags & TypeFlags.Index || + isGenericMappedType(objectType)) { + // If either the object type or the index type are type parameters, or if the object type is a mapped + // type with a generic constraint, we are performing a higher-order index access where we cannot + // meaningfully access the properties of the object type. In those cases, we first check that the + // index type is assignable to 'keyof T' for the object type. + if (accessNode) { + const keyType = indexType.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(indexType) || emptyObjectType : indexType; + if (!isTypeAssignableTo(keyType, getIndexType(objectType))) { + error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); + return unknownType; + } } - return getIndexedAccessTypeForTypeParameter(objectType, indexType); + // If the object type is a mapped type { [P in K]: E }, we instantiate E using a mapper that substitutes + // the index type for P. For example, for an index access { [P in K]: Box }[X], we construct the + // type Box. + if (isGenericMappedType(objectType)) { + return getIndexedAccessForMappedType(objectType, indexType, accessNode); + } + // Otherwise we defer the operation by creating an indexed access type. + const id = objectType.id + "," + indexType.id; + return indexedAccessTypes[id] || (indexedAccessTypes[id] = createIndexedAccessType(objectType, indexType)); } const apparentType = getApparentType(objectType); if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Primitive)) { @@ -7153,12 +7178,24 @@ namespace ts { } if (target.flags & TypeFlags.TypeParameter) { - // Given a type parameter K with a constraint keyof T, a type S is - // assignable to K if S is assignable to keyof T. - const constraint = getConstraintOfTypeParameter(target); - if (constraint && constraint.flags & TypeFlags.Index) { - if (result = isRelatedTo(source, constraint, reportErrors)) { - return result; + // A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P]. + if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(source) === getIndexType(target)) { + if (!(source).declaration.questionToken) { + const templateType = getTemplateTypeFromMappedType(source); + const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source)); + if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) { + return result; + } + } + } + else { + // Given a type parameter K with a constraint keyof T, a type S is + // assignable to K if S is assignable to keyof T. + const constraint = getConstraintOfTypeParameter(target); + if (constraint && constraint.flags & TypeFlags.Index) { + if (result = isRelatedTo(source, constraint, reportErrors)) { + return result; + } } } } @@ -7178,22 +7215,41 @@ namespace ts { } } } + else if (target.flags & TypeFlags.IndexedAccess) { + // if we have indexed access types with identical index types, see if relationship holds for + // the two object types. + if (source.flags & TypeFlags.IndexedAccess && (source).indexType === (target).indexType) { + if (result = isRelatedTo((source).objectType, (target).objectType, reportErrors)) { + return result; + } + } + } if (source.flags & TypeFlags.TypeParameter) { - let constraint = getConstraintOfTypeParameter(source); - - if (!constraint || constraint.flags & TypeFlags.Any) { - constraint = emptyObjectType; + // A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X. + if (getObjectFlags(target) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(target) === getIndexType(source)) { + const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(target)); + const templateType = getTemplateTypeFromMappedType(target); + if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) { + return result; + } } + else { + let constraint = getConstraintOfTypeParameter(source); - // The constraint may need to be further instantiated with its 'this' type. - constraint = getTypeWithThisArgument(constraint, source); + if (!constraint || constraint.flags & TypeFlags.Any) { + constraint = emptyObjectType; + } - // Report constraint errors only if the constraint is not the empty object type - const reportConstraintErrors = reportErrors && constraint !== emptyObjectType; - if (result = isRelatedTo(constraint, target, reportConstraintErrors)) { - errorInfo = saveErrorInfo; - return result; + // The constraint may need to be further instantiated with its 'this' type. + constraint = getTypeWithThisArgument(constraint, source); + + // Report constraint errors only if the constraint is not the empty object type + const reportConstraintErrors = reportErrors && constraint !== emptyObjectType; + if (result = isRelatedTo(constraint, target, reportConstraintErrors)) { + errorInfo = saveErrorInfo; + return result; + } } } else { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2dd4e76f8f..68e7bb6e98 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1731,7 +1731,7 @@ "category": "Error", "code": 2535 }, - "Type '{0}' is not constrained to 'keyof {1}'.": { + "Type '{0}' cannot be used to index type '{1}'.": { "category": "Error", "code": 2536 }, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b5e3eefbb2..2b8b6fe294 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2974,8 +2974,6 @@ namespace ts { /* @internal */ resolvedIndexType: IndexType; /* @internal */ - resolvedIndexedAccessTypes: IndexedAccessType[]; - /* @internal */ isThisType?: boolean; } @@ -2985,7 +2983,7 @@ namespace ts { export interface IndexedAccessType extends Type { objectType: Type; - indexType: TypeParameter; + indexType: Type; } export const enum SignatureKind {