From 671f7a8dae85564e5ec846af80ca80862dfd101f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 6 Dec 2016 14:41:38 -0800 Subject: [PATCH] Defer indexed access T[K] where T is generic and K is non-generic --- src/compiler/checker.ts | 94 +++++++++++++++++++++++++++++------------ src/compiler/types.ts | 4 +- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 912db011904..35e034cebed 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4667,33 +4667,24 @@ namespace ts { * The apparent type of a type parameter is the base constraint instantiated with the type parameter * as the type argument for the 'this' type. */ - function getApparentTypeOfTypeParameter(type: TypeParameter) { + function getApparentTypeOfTypeVariable(type: TypeVariable) { if (!type.resolvedApparentType) { - let constraintType = getConstraintOfTypeParameter(type); + let constraintType = getConstraintOfTypeVariable(type); while (constraintType && constraintType.flags & TypeFlags.TypeParameter) { - constraintType = getConstraintOfTypeParameter(constraintType); + constraintType = getConstraintOfTypeVariable(constraintType); } type.resolvedApparentType = getTypeWithThisArgument(constraintType || emptyObjectType, type); } return type.resolvedApparentType; } - /** - * The apparent type of an indexed access T[K] is the type of T's string index signature, if any. - */ - function getApparentTypeOfIndexedAccess(type: IndexedAccessType) { - return getIndexTypeOfType(getApparentType(type.objectType), IndexKind.String) || type; - } - /** * For a type parameter, return the base constraint of the type parameter. For the string, number, * boolean, and symbol primitive types, return the corresponding object types. Otherwise return the * type itself. Note that the apparent type of a union type is the union type itself. */ function getApparentType(type: Type): Type { - const t = type.flags & TypeFlags.TypeParameter ? getApparentTypeOfTypeParameter(type) : - type.flags & TypeFlags.IndexedAccess ? getApparentTypeOfIndexedAccess(type) : - type; + const t = type.flags & TypeFlags.TypeVariable ? getApparentTypeOfTypeVariable(type) : type; return t.flags & TypeFlags.StringLike ? globalStringType : t.flags & TypeFlags.NumberLike ? globalNumberType : t.flags & TypeFlags.BooleanLike ? globalBooleanType : @@ -5279,6 +5270,31 @@ namespace ts { return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint; } + function getConstraintOfIndexedAccess(type: IndexedAccessType): Type { + // The constraint of T[K], where T is an object, union, or intersection type, + // is the type of the string index signature of T, if any. + if (type.objectType.flags & TypeFlags.StructuredType) { + return getIndexTypeOfType(type.objectType, IndexKind.String); + } + // The constraint of T[K], where T is a type variable, is A[K], where A is the + // apparent type of T. + if (type.objectType.flags & TypeFlags.TypeVariable) { + const apparentType = getApparentTypeOfTypeVariable(type.objectType); + if (apparentType !== emptyObjectType) { + return isTypeOfKind((type).indexType, TypeFlags.StringLike) ? + getIndexedAccessType(apparentType, (type).indexType) : + getIndexTypeOfType(apparentType, IndexKind.String); + } + } + return undefined; + } + + function getConstraintOfTypeVariable(type: TypeVariable): Type { + return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(type) : + type.flags & TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(type) : + undefined; + } + function getParentSymbolOfTypeParameter(typeParameter: TypeParameter): Symbol { return getSymbolOfNode(getDeclarationOfKind(typeParameter.symbol, SyntaxKind.TypeParameter).parent); } @@ -6032,11 +6048,16 @@ namespace ts { } function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) { - if (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) || isGenericMappedType(objectType)) { - // If the index type is generic 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 the index type is generic, if the object type is generic and doesn't originate in an expression, + // 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. 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 (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) || + maybeTypeOfKind(objectType, TypeFlags.TypeVariable) && !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) || + isGenericMappedType(objectType)) { + // We first check that the index type is assignable to 'keyof T' for the object type. if (accessNode) { if (!isTypeAssignableTo(indexType, getIndexType(objectType))) { error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); @@ -6053,6 +6074,7 @@ namespace ts { const id = objectType.id + "," + indexType.id; return indexedAccessTypes[id] || (indexedAccessTypes[id] = createIndexedAccessType(objectType, indexType)); } + // In the following we resolve T[K] to the type of the property in T selected by K. const apparentObjectType = getApparentType(objectType); if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Primitive)) { const propTypes: Type[] = []; @@ -7240,8 +7262,7 @@ namespace ts { return result; } } - - if (target.flags & TypeFlags.TypeParameter) { + else if (target.flags & TypeFlags.TypeParameter) { // 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) { @@ -7270,10 +7291,10 @@ namespace ts { return result; } } - // Given a type parameter T with a constraint C, a type S is assignable to + // Given a type variable T with a constraint C, a type S is assignable to // keyof T if S is assignable to keyof C. - if ((target).type.flags & TypeFlags.TypeParameter) { - const constraint = getConstraintOfTypeParameter((target).type); + if ((target).type.flags & TypeFlags.TypeVariable) { + const constraint = getConstraintOfTypeVariable((target).type); if (constraint) { if (result = isRelatedTo(source, getIndexType(constraint), reportErrors)) { return result; @@ -7289,6 +7310,15 @@ namespace ts { return result; } } + // 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 = getConstraintOfIndexedAccess(target); + if (constraint) { + if (result = isRelatedTo(source, constraint, reportErrors)) { + errorInfo = saveErrorInfo; + return result; + } + } } if (source.flags & TypeFlags.TypeParameter) { @@ -7297,6 +7327,7 @@ namespace ts { const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(target)); const templateType = getTemplateTypeFromMappedType(target); if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) { + errorInfo = saveErrorInfo; return result; } } @@ -7318,6 +7349,17 @@ 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 = getConstraintOfIndexedAccess(source); + if (constraint) { + if (result = isRelatedTo(constraint, target, reportErrors)) { + errorInfo = saveErrorInfo; + return result; + } + } + } else { if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (source).target === (target).target) { // We have type references to same target type, see if relationship holds for all type arguments @@ -14978,8 +15020,8 @@ namespace ts { function isLiteralContextualType(contextualType: Type) { if (contextualType) { - if (contextualType.flags & TypeFlags.TypeParameter) { - const apparentType = getApparentTypeOfTypeParameter(contextualType); + if (contextualType.flags & TypeFlags.TypeVariable) { + const apparentType = getApparentTypeOfTypeVariable(contextualType); // If the type parameter is constrained to the base primitive type we're checking for, // consider this a literal context. For example, given a type parameter 'T extends string', // this causes us to infer string literal types for T. @@ -15814,7 +15856,7 @@ namespace ts { checkSourceElement(node.type); const type = getTypeFromMappedTypeNode(node); const constraintType = getConstraintTypeFromMappedType(type); - const keyType = constraintType.flags & TypeFlags.TypeParameter ? getApparentTypeOfTypeParameter(constraintType) : constraintType; + const keyType = constraintType.flags & TypeFlags.TypeVariable ? getApparentTypeOfTypeVariable(constraintType) : constraintType; checkTypeAssignableTo(keyType, stringType, node.typeParameter.constraint); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0ad84d11c35..0d15c758fd6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2967,6 +2967,8 @@ namespace ts { } export interface TypeVariable extends Type { + /* @internal */ + resolvedApparentType: Type; /* @internal */ resolvedIndexType: IndexType; } @@ -2979,8 +2981,6 @@ namespace ts { /* @internal */ mapper?: TypeMapper; // Instantiation mapper /* @internal */ - resolvedApparentType: Type; - /* @internal */ isThisType?: boolean; }