diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 13c50d3b770b3..ba4dfd0b577ff 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -774,6 +774,7 @@ namespace ts { const stringMappingTypes = new Map(); const substitutionTypes = new Map(); const subtypeReductionCache = new Map(); + const cachedTypes = new Map(); const evolvingArrayTypes: EvolvingArrayType[] = []; const undefinedProperties: SymbolTable = new Map(); const markerTypes = new Set(); @@ -1061,6 +1062,15 @@ namespace ts { return checker; + function getCachedType(key: string | undefined) { + return key ? cachedTypes.get(key) : undefined; + } + + function setCachedType(key: string | undefined, type: Type) { + if (key) cachedTypes.set(key, type); + return type; + } + function getJsxNamespace(location: Node | undefined): __String { if (location) { const file = getSourceFileOfNode(location); @@ -21295,10 +21305,15 @@ namespace ts { type.flags & TypeFlags.NumberLiteral ? numberType : type.flags & TypeFlags.BigIntLiteral ? bigintType : type.flags & TypeFlags.BooleanLiteral ? booleanType : - type.flags & TypeFlags.Union ? mapType(type as UnionType, getBaseTypeOfLiteralType) : + type.flags & TypeFlags.Union ? getBaseTypeOfLiteralTypeUnion(type as UnionType) : type; } + function getBaseTypeOfLiteralTypeUnion(type: UnionType) { + const key = `B${getTypeId(type)}`; + return getCachedType(key) ?? setCachedType(key, mapType(type, getBaseTypeOfLiteralType)); + } + function getWidenedLiteralType(type: Type): Type { return type.flags & TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(type as LiteralType) : type.flags & TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType : @@ -23531,23 +23546,25 @@ namespace ts { // For example, when a variable of type number | string | boolean is assigned a value of type number | boolean, // we remove type string. function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) { - if (declaredType !== assignedType) { - if (assignedType.flags & TypeFlags.Never) { - return assignedType; - } - let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); - if (assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType)) { - reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types - } - // Our crude heuristic produces an invalid result in some cases: see GH#26130. - // For now, when that happens, we give up and don't narrow at all. (This also - // means we'll never narrow for erroneous assignments where the assigned type - // is not assignable to the declared type.) - if (isTypeAssignableTo(assignedType, reducedType)) { - return reducedType; - } + if (declaredType === assignedType) { + return declaredType; } - return declaredType; + if (assignedType.flags & TypeFlags.Never) { + return assignedType; + } + const key = `A${getTypeId(declaredType)},${getTypeId(assignedType)}`; + return getCachedType(key) ?? setCachedType(key, getAssignmentReducedTypeWorker(declaredType, assignedType)); + } + + function getAssignmentReducedTypeWorker(declaredType: UnionType, assignedType: Type) { + const filteredType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); + // Ensure that we narrow to fresh types if the assignment is a fresh boolean literal type. + const reducedType = assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType) ? mapType(filteredType, getFreshTypeOfLiteralType) : filteredType; + // Our crude heuristic produces an invalid result in some cases: see GH#26130. + // For now, when that happens, we give up and don't narrow at all. (This also + // means we'll never narrow for erroneous assignments where the assigned type + // is not assignable to the declared type.) + return isTypeAssignableTo(assignedType, reducedType) ? reducedType : declaredType; } function isFunctionObjectType(type: ObjectType): boolean { @@ -25041,7 +25058,7 @@ namespace ts { const targetType = hasStaticModifier(Debug.checkDefined(symbol.valueDeclaration, "should always have a declaration")) ? getTypeOfSymbol(classSymbol) as InterfaceType : getDeclaredTypeOfSymbol(classSymbol); - return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); + return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true); } function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { @@ -25315,10 +25332,16 @@ namespace ts { if (!nonConstructorTypeInUnion) return type; } - return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); + return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true); + } + + function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) { + const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0)}` : undefined; + return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived)); } - function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, isRelated: (source: Type, target: Type) => boolean) { + function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) { + const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf; if (!assumeTrue) { return filterType(type, t => !isRelated(t, candidate)); } @@ -25330,7 +25353,6 @@ namespace ts { return assignableType; } } - // If the candidate type is a subtype of the target type, narrow to the candidate type. // Otherwise, if the target type is assignable to the candidate type, keep the target type. // Otherwise, if the candidate type is assignable to the target type, narrow to the candidate @@ -25369,7 +25391,7 @@ namespace ts { const predicateArgument = getTypePredicateArgument(predicate, callExpression); if (predicateArgument) { if (isMatchingReference(reference, predicateArgument)) { - return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf); + return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false); } if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { @@ -25377,7 +25399,7 @@ namespace ts { } const access = getDiscriminantPropertyAccess(predicateArgument, type); if (access) { - return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf)); + return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false)); } } }