diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index febb8ffdad3..d96eb6a6c4d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7188,36 +7188,39 @@ namespace ts { // Return the lower bound of the key type in a mapped type. Intuitively, the lower // bound includes those keys that are known to always be present, for example because // because of constraints on type parameters (e.g. 'keyof T' for a constrained T). - function getLowerBoundOfKeyType(type: Type): Type { + // The isIndexType flag indicates that the type is the index type of an indexed + // access that is the target of an assignment. + function getLowerBoundOfKeyType(type: Type, isIndexType: boolean): Type { if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) { return type; } if (type.flags & TypeFlags.Index) { - return getIndexType(getApparentType((type).type)); + const keys = getIndexType(getApparentType((type).type)); + return isIndexType ? filterType(keys, t => !!(t.flags & TypeFlags.Literal)) : keys; } if (type.flags & TypeFlags.Conditional) { - return getLowerBoundOfConditionalType(type); + if ((type).root.isDistributive) { + const checkType = (type).checkType; + const constraint = getLowerBoundOfKeyType(checkType, isIndexType); + if (constraint !== checkType) { + const mapper = makeUnaryTypeMapper((type).root.checkType, constraint); + return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, (type).mapper)); + } + } + return type; } if (type.flags & TypeFlags.Union) { - return getUnionType(sameMap((type).types, getLowerBoundOfKeyType)); + return getUnionType(sameMap((type).types, t => getLowerBoundOfKeyType(t, isIndexType))); } if (type.flags & TypeFlags.Intersection) { - return getIntersectionType(sameMap((type).types, getLowerBoundOfKeyType)); + return getIntersectionType(sameMap((type).types, t => getLowerBoundOfKeyType(t, isIndexType))); + } + if (isIndexType && type.flags & TypeFlags.Instantiable) { + return getLowerBoundOfKeyType(getConstraintOfType(type) || neverType, isIndexType); } return neverType; } - function getLowerBoundOfConditionalType(type: ConditionalType) { - if (type.root.isDistributive) { - const constraint = getLowerBoundOfKeyType(type.checkType); - if (constraint !== type.checkType) { - const mapper = makeUnaryTypeMapper(type.root.checkType, constraint); - return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper)); - } - } - return type; - } - /** Resolve the members of a mapped type { [P in K]: T } */ function resolveMappedTypeMembers(type: MappedType) { const members: SymbolTable = createSymbolTable(); @@ -7246,7 +7249,7 @@ namespace ts { } } else { - forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); + forEachType(getLowerBoundOfKeyType(constraintType, /*isIndexType*/ false), addMemberForKeyType); } setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); @@ -7523,7 +7526,7 @@ namespace ts { // a union - once negated types exist and are applied to the conditional false branch, this "constraint" // likely doesn't need to exist. if (type.root.isDistributive && type.restrictiveInstantiation !== type) { - const simplified = getSimplifiedType(type.checkType); + const simplified = getSimplifiedType(type.checkType, /*writing*/ false); const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified; if (constraint && constraint !== type.checkType) { const mapper = makeUnaryTypeMapper(type.root.checkType, constraint); @@ -7630,7 +7633,7 @@ namespace ts { return t.immediateBaseConstraint = noConstraintType; } constraintDepth++; - let result = computeBaseConstraint(getSimplifiedType(t)); + let result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false)); constraintDepth--; if (!popTypeResolution()) { if (t.flags & TypeFlags.TypeParameter) { @@ -10003,53 +10006,68 @@ namespace ts { return maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive | TypeFlags.Index); } - function getSimplifiedType(type: Type): Type { - return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type) : type; + function getSimplifiedType(type: Type, writing: boolean): Type { + return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type, writing) : type; } - function distributeIndexOverObjectType(objectType: Type, indexType: Type) { - // (T | U)[K] -> T[K] | U[K] - if (objectType.flags & TypeFlags.Union) { - return mapType(objectType, t => getSimplifiedType(getIndexedAccessType(t, indexType))); - } + function distributeIndexOverObjectType(objectType: Type, indexType: Type, writing: boolean) { + // (T | U)[K] -> T[K] | U[K] (reading) + // (T | U)[K] -> T[K] & U[K] (writing) // (T & U)[K] -> T[K] & U[K] - if (objectType.flags & TypeFlags.Intersection) { - return getIntersectionType(map((objectType as IntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType)))); + if (objectType.flags & TypeFlags.UnionOrIntersection) { + const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing)); + return objectType.flags & TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types); + } + } + + function distributeObjectOverIndexType(objectType: Type, indexType: Type, writing: boolean) { + // T[A | B] -> T[A] | T[B] (reading) + // T[A | B] -> T[A] & T[B] (writing) + if (indexType.flags & TypeFlags.Union) { + const types = map((indexType as UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing)); + return writing ? getIntersectionType(types) : getUnionType(types); } } // Transform an indexed access to a simpler form, if possible. Return the simpler form, or return - // the type itself if no transformation is possible. - function getSimplifiedIndexedAccessType(type: IndexedAccessType): Type { - if (type.simplified) { - return type.simplified === circularConstraintType ? type : type.simplified; + // the type itself if no transformation is possible. The writing flag indicates that the type is + // the target of an assignment. + function getSimplifiedIndexedAccessType(type: IndexedAccessType, writing: boolean): Type { + const cache = writing ? "simplifiedForWriting" : "simplifiedForReading"; + if (type[cache]) { + return type[cache] === circularConstraintType ? type : type[cache]!; } - type.simplified = circularConstraintType; + type[cache] = circularConstraintType; // We recursively simplify the object type as it may in turn be an indexed access type. For example, with // '{ [P in T]: { [Q in U]: number } }[T][U]' we want to first simplify the inner indexed access type. - const objectType = getSimplifiedType(type.objectType); - const indexType = getSimplifiedType(type.indexType); - // T[A | B] -> T[A] | T[B] - if (indexType.flags & TypeFlags.Union) { - return type.simplified = mapType(indexType, t => getSimplifiedType(getIndexedAccessType(objectType, t))); + const objectType = getSimplifiedType(type.objectType, writing); + const indexType = getSimplifiedType(type.indexType, writing); + // T[A | B] -> T[A] | T[B] (reading) + // T[A | B] -> T[A] & T[B] (writing) + const distributedOverIndex = distributeObjectOverIndexType(objectType, indexType, writing); + if (distributedOverIndex) { + return type[cache] = distributedOverIndex; } // Only do the inner distributions if the index can no longer be instantiated to cause index distribution again if (!(indexType.flags & TypeFlags.Instantiable)) { - const simplified = distributeIndexOverObjectType(objectType, indexType); - if (simplified) { - return type.simplified = simplified; + // (T | U)[K] -> T[K] | U[K] (reading) + // (T | U)[K] -> T[K] & U[K] (writing) + // (T & U)[K] -> T[K] & U[K] + const distributedOverObject = distributeIndexOverObjectType(objectType, indexType, writing); + if (distributedOverObject) { + return type[cache] = distributedOverObject; } } - // So ultimately: + // So ultimately (reading): // ((A & B) | C)[K1 | K2] -> ((A & B) | C)[K1] | ((A & B) | C)[K2] -> (A & B)[K1] | C[K1] | (A & B)[K2] | C[K2] -> (A[K1] & B[K1]) | C[K1] | (A[K2] & B[K2]) | C[K2] // 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 }[X], we // construct the type Box. if (isGenericMappedType(objectType)) { - return type.simplified = mapType(substituteIndexedMappedType(objectType, type.indexType), getSimplifiedType); + return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing)); } - return type.simplified = type; + return type[cache] = type; } function substituteIndexedMappedType(objectType: MappedType, index: Type) { @@ -12197,10 +12215,10 @@ namespace ts { target = (target).typeVariable; } if (source.flags & TypeFlags.IndexedAccess) { - source = getSimplifiedType(source); + source = getSimplifiedType(source, /*writing*/ false); } if (target.flags & TypeFlags.IndexedAccess) { - target = getSimplifiedType(target); + target = getSimplifiedType(target, /*writing*/ true); } // Try to see if we're relating something like `Foo` -> `Bar | null | undefined`. @@ -12780,7 +12798,7 @@ namespace ts { } // A type S is assignable to keyof T if S is assignable to keyof C, where C is the // simplified form of T or, if T doesn't simplify, the constraint of T. - const simplified = getSimplifiedType((target).type); + const simplified = getSimplifiedType((target).type, /*writing*/ false); const constraint = simplified !== (target).type ? simplified : getConstraintOfType((target).type); if (constraint) { // We require Ternary.True here such that circular constraints don't cause @@ -12795,12 +12813,26 @@ namespace ts { else if (target.flags & TypeFlags.IndexedAccess) { // A type S is related to a type T[K], where T and K aren't both type variables, if S is related to C, // where C is the base constraint of T[K] - if (relation !== identityRelation && - !(isGenericObjectType((target).objectType) && isGenericIndexType((target).indexType))) { - const constraint = getBaseConstraintOfType(target); - if (constraint && constraint !== target) { - if (result = isRelatedTo(source, constraint, reportErrors)) { - return result; + if (relation !== identityRelation) { + const objectType = (target).objectType + const indexType = (target).indexType; + if (indexType.flags & TypeFlags.StructuredOrInstantiable) { + const keyType = getLowerBoundOfKeyType(indexType, /*isIndexType*/ true); + if (keyType !== indexType && !(keyType.flags & TypeFlags.Never)) { + const targetType = keyType.flags & TypeFlags.Union ? + getIntersectionType(map((keyType).types, t => getIndexedAccessType(objectType, t))) : + getIndexedAccessType(objectType, keyType); + if (result = isRelatedTo(source, targetType, reportErrors)) { + return result; + } + } + } + else { + const constraint = getConstraintOfType(objectType); + if (constraint) { + if (result = isRelatedTo(source, getIndexedAccessType(constraint, indexType), reportErrors)) { + return result; + } } } } @@ -14660,16 +14692,16 @@ namespace ts { } else { // Infer to the simplified version of an indexed access, if possible, to (hopefully) expose more bare type parameters to the inference engine - const simplified = getSimplifiedType(target); + const simplified = getSimplifiedType(target, /*writing*/ false); if (simplified !== target) { inferFromTypesOnce(source, simplified); } else if (target.flags & TypeFlags.IndexedAccess) { - const indexType = getSimplifiedType((target as IndexedAccessType).indexType); + const indexType = getSimplifiedType((target as IndexedAccessType).indexType, /*writing*/ false); // Generally simplifications of instantiable indexes are avoided to keep relationship checking correct, however if our target is an access, we can consider // that key of that access to be "instantiated", since we're looking to find the infernce goal in any way we can. if (indexType.flags & TypeFlags.Instantiable) { - const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType), indexType); + const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false); if (simplified && simplified !== target) { inferFromTypesOnce(source, simplified); } @@ -15697,24 +15729,19 @@ namespace ts { if (!(type.flags & TypeFlags.Union)) { return mapper(type); } - const types = (type).types; - let mappedType: Type | undefined; let mappedTypes: Type[] | undefined; - for (const current of types) { - const t = mapper(current); - if (t) { - if (!mappedType) { - mappedType = t; - } - else if (!mappedTypes) { - mappedTypes = [mappedType, t]; + for (const t of (type).types) { + const mapped = mapper(t); + if (mapped) { + if (!mappedTypes) { + mappedTypes = [mapped]; } else { - mappedTypes.push(t); + mappedTypes.push(mapped); } } } - return mappedTypes ? getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal) : mappedType; + return mappedTypes && getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal); } function extractTypesOfKind(type: Type, kind: TypeFlags) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index bcc3218a017..7c9b7cee1e5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4279,7 +4279,8 @@ namespace ts { objectType: Type; indexType: Type; constraint?: Type; - simplified?: Type; + simplifiedForReading?: Type; + simplifiedForWriting?: Type; } export type TypeVariable = TypeParameter | IndexedAccessType;