From 8eb24db0c0a0dae47e2696095d46f894546105aa Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 29 Jul 2018 15:14:01 -0700 Subject: [PATCH] Homomorphic mapped type support for arrays and tuples --- src/compiler/checker.ts | 65 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2b255da7ae3..a708e5e86b6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8432,6 +8432,10 @@ namespace ts { return createTypeFromGenericGlobalType(globalArrayType, [elementType]); } + function createReadonlyArrayType(elementType: Type): ObjectType { + return createTypeFromGenericGlobalType(globalReadonlyArrayType, [elementType]); + } + function getTypeFromArrayTypeNode(node: ArrayTypeNode): Type { const links = getNodeLinks(node); if (!links.resolvedType) { @@ -10087,11 +10091,16 @@ namespace ts { } function instantiateMappedType(type: MappedType, mapper: TypeMapper): Type { - // Check if we have a homomorphic mapped type, i.e. a type of the form { [P in keyof T]: X } for some - // type variable T. If so, the mapped type is distributive over a union type and when T is instantiated - // to a union type A | B, we produce { [P in keyof A]: X } | { [P in keyof B]: X }. Furthermore, for - // homomorphic mapped types we leave primitive types alone. For example, when T is instantiated to a - // union type A | undefined, we produce { [P in keyof A]: X } | undefined. + // For a momomorphic mapped type { [P in keyof T]: X }, where T is some type variable, the mapping + // operation depends on T as follows: + // * If T is a primitive type no mapping is performed and the result is simply T. + // * If T is a union type we distribute the mapped type over the union. + // * If T is an array we map to an array where the element type has been transformed. + // * If T is a tuple we map to a tuple where the element types have been transformed. + // * Otherwise we map to an object type where the type of each property has been transformed. + // For example, when T is instantiated to a union type A | B, we produce { [P in keyof A]: X } | + // { [P in keyof B]: X }, and when when T is instantiated to a union type A | undefined, we produce + // { [P in keyof A]: X } | undefined. const constraintType = getConstraintTypeFromMappedType(type); if (constraintType.flags & TypeFlags.Index) { const typeVariable = (constraintType).type; @@ -10100,7 +10109,11 @@ namespace ts { if (typeVariable !== mappedTypeVariable) { return mapType(mappedTypeVariable, t => { if (isMappableType(t)) { - return instantiateAnonymousType(type, createReplacementMapper(typeVariable, t, mapper)); + const replacementMapper = createReplacementMapper(typeVariable, t, mapper); + return isArrayType(t) ? createArrayType(instantiateMappedTypeTemplate(type, numberType, /*isOptional*/ true, replacementMapper)) : + isReadonlyArrayType(t) ? createReadonlyArrayType(instantiateMappedTypeTemplate(type, numberType, /*isOptional*/ true, replacementMapper)) : + isTupleType(t) ? instantiateMappedTupleType(t, type, replacementMapper) : + instantiateAnonymousType(type, replacementMapper); } return t; }); @@ -10114,6 +10127,26 @@ namespace ts { return type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection); } + function instantiateMappedTupleType(tupleType: TupleTypeReference, mappedType: MappedType, mapper: TypeMapper) { + const minLength = tupleType.target.minLength; + const elementTypes = map(tupleType.typeArguments || emptyArray, (_, i) => + instantiateMappedTypeTemplate(mappedType, getLiteralType("" + i), i >= minLength, mapper)); + const modifiers = getMappedTypeModifiers(mappedType); + const newMinLength = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : + modifiers & MappedTypeModifiers.ExcludeOptional ? getTypeReferenceArity(tupleType) - (tupleType.target.hasRestElement ? 1 : 0) : + minLength; + return createTupleType(elementTypes, newMinLength, tupleType.target.hasRestElement, tupleType.target.associatedNames); + } + + function instantiateMappedTypeTemplate(type: MappedType, key: Type, isOptional: boolean, mapper: TypeMapper) { + const templateMapper = combineTypeMappers(mapper, createTypeMapper([getTypeParameterFromMappedType(type)], [key])); + const propType = instantiateType(getTemplateTypeFromMappedType(type.target || type), templateMapper); + const modifiers = getMappedTypeModifiers(type); + return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !isTypeAssignableTo(undefinedType, propType) ? getOptionalType(propType) : + strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : + propType; + } + function instantiateAnonymousType(type: AnonymousType, mapper: TypeMapper): AnonymousType { const result = createObjectType(type.objectFlags | ObjectFlags.Instantiated, type.symbol); if (type.objectFlags & ObjectFlags.Mapped) { @@ -12441,6 +12474,10 @@ namespace ts { return !!(getObjectFlags(type) & ObjectFlags.Reference) && (type).target === globalArrayType; } + function isReadonlyArrayType(type: Type): boolean { + return !!(getObjectFlags(type) & ObjectFlags.Reference) && (type).target === globalReadonlyArrayType; + } + function isArrayLikeType(type: Type): boolean { // A type is array-like if it is a reference to the global Array or global ReadonlyArray type, // or if it is not the undefined or null type and if it is assignable to ReadonlyArray @@ -12996,6 +13033,22 @@ namespace ts { return undefined; } } + // For arrays and tuples we infer new arrays and tuples where the reverse mapping has been + // applied to the element type(s). + if (isArrayType(source)) { + return createArrayType(inferReverseMappedType((source).typeArguments![0], target)); + } + if (isReadonlyArrayType(source)) { + return createReadonlyArrayType(inferReverseMappedType((source).typeArguments![0], target)); + } + if (isTupleType(source)) { + const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target)); + const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? + getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength; + return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.associatedNames); + } + // For all other object types we infer a new object type where the reverse mapping has been + // applied to the type of each property. const reversed = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; reversed.source = source; reversed.mappedType = target;