Skip to content

Commit 29edef1

Browse files
committed
Improve CFA for truthy, equality, and typeof checks
1 parent a95de48 commit 29edef1

File tree

1 file changed

+48
-31
lines changed

1 file changed

+48
-31
lines changed

src/compiler/checker.ts

+48-31
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,8 @@ namespace ts {
848848
emptyTypeLiteralSymbol.members = createSymbolTable();
849849
const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, emptyArray, emptyArray, emptyArray);
850850

851+
const unknownUnionType = strictNullChecks ? getUnionType([undefinedType, nullType, createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray)]) : unknownType;
852+
851853
const emptyGenericType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray) as ObjectType as GenericType;
852854
emptyGenericType.instantiations = new Map<string, TypeReference>();
853855

@@ -14835,6 +14837,7 @@ namespace ts {
1483514837
t.flags & TypeFlags.Number && includes & TypeFlags.NumberLiteral ||
1483614838
t.flags & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral ||
1483714839
t.flags & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol ||
14840+
t.flags & TypeFlags.Void && includes & TypeFlags.Undefined ||
1483814841
t.flags & TypeFlags.NonPrimitive && includes & TypeFlags.Object ||
1483914842
isEmptyAnonymousObjectType(t) && includes & TypeFlags.DefinitelyNonNullable;
1484014843
if (remove) {
@@ -14998,6 +15001,7 @@ namespace ts {
1499815001
includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral ||
1499915002
includes & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral ||
1500015003
includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol ||
15004+
includes & TypeFlags.Void && includes & TypeFlags.Undefined ||
1500115005
includes & TypeFlags.NonPrimitive && includes & TypeFlags.Object ||
1500215006
includes & TypeFlags.IncludesEmptyObject && includes & TypeFlags.DefinitelyNonNullable) {
1500315007
removeRedundantSupertypes(typeSet, includes);
@@ -18183,7 +18187,7 @@ namespace ts {
1818318187
// Since unions and intersections may reduce to `never`, we exclude them here.
1818418188
if (s & TypeFlags.Undefined && (!strictNullChecks && !(t & TypeFlags.UnionOrIntersection) || t & (TypeFlags.Undefined | TypeFlags.Void))) return true;
1818518189
if (s & TypeFlags.Null && (!strictNullChecks && !(t & TypeFlags.UnionOrIntersection) || t & TypeFlags.Null)) return true;
18186-
if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) return true;
18190+
if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive && !(relation === strictSubtypeRelation && isEmptyAnonymousObjectType(source))) return true;
1818718191
if (relation === assignableRelation || relation === comparableRelation) {
1818818192
if (s & TypeFlags.Any) return true;
1818918193
// Type number or any numeric literal type is assignable to any numeric enum type or any
@@ -23505,6 +23509,24 @@ namespace ts {
2350523509
return filterType(type, t => (getTypeFacts(t) & include) !== 0);
2350623510
}
2350723511

23512+
function getIntersectionWithFacts(type: Type, facts: TypeFacts) {
23513+
const reduced = getTypeWithFacts(strictNullChecks && type.flags & TypeFlags.Unknown ? unknownUnionType : type, facts);
23514+
if (strictNullChecks) {
23515+
switch (facts) {
23516+
case TypeFacts.NEUndefined:
23517+
const emptyOrNull = maybeTypeOfKind(reduced, TypeFlags.Null) ? emptyObjectType : getUnionType([emptyObjectType, nullType]);
23518+
return mapType(reduced, t => getTypeFacts(t) & TypeFacts.EQUndefined ? getIntersectionType([t, getTypeFacts(t) & TypeFacts.EQNull ? emptyOrNull : emptyObjectType]): t);
23519+
case TypeFacts.NENull:
23520+
const emptyOrUndefined = maybeTypeOfKind(reduced, TypeFlags.Undefined) ? emptyObjectType : getUnionType([emptyObjectType, undefinedType]);
23521+
return mapType(reduced, t => getTypeFacts(t) & TypeFacts.EQNull ? getIntersectionType([t, getTypeFacts(t) & TypeFacts.EQUndefined ? emptyOrUndefined : emptyObjectType]): t);
23522+
case TypeFacts.NEUndefinedOrNull:
23523+
case TypeFacts.Truthy:
23524+
return mapType(reduced, t => getTypeFacts(t) & TypeFacts.EQUndefinedOrNull ? getIntersectionType([t, emptyObjectType]): t);
23525+
}
23526+
}
23527+
return reduced;
23528+
}
23529+
2350823530
function getTypeWithDefault(type: Type, defaultExpression: Expression) {
2350923531
return defaultExpression ?
2351023532
getUnionType([getNonUndefinedType(type), getTypeOfExpression(defaultExpression)]) :
@@ -24637,6 +24659,9 @@ namespace ts {
2463724659
return getEvolvingArrayType(getUnionType(map(types, getElementTypeOfEvolvingArrayType)));
2463824660
}
2463924661
const result = getUnionType(sameMap(types, finalizeEvolvingArrayType), subtypeReduction);
24662+
if (result === unknownUnionType) {
24663+
return unknownType;
24664+
}
2464024665
if (result !== declaredType && result.flags & declaredType.flags & TypeFlags.Union && arraysEqual((result as UnionType).types, (declaredType as UnionType).types)) {
2464124666
return declaredType;
2464224667
}
@@ -24744,8 +24769,7 @@ namespace ts {
2474424769

2474524770
function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
2474624771
if (isMatchingReference(reference, expr)) {
24747-
return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType :
24748-
getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
24772+
return getIntersectionWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
2474924773
}
2475024774
if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) {
2475124775
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
@@ -24905,9 +24929,6 @@ namespace ts {
2490524929
assumeTrue = !assumeTrue;
2490624930
}
2490724931
const valueType = getTypeOfExpression(value);
24908-
if (assumeTrue && (type.flags & TypeFlags.Unknown) && (operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken) && (valueType.flags & TypeFlags.Null)) {
24909-
return getUnionType([nullType, undefinedType]);
24910-
}
2491124932
if ((type.flags & TypeFlags.Unknown) && assumeTrue && (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) {
2491224933
if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) {
2491324934
return valueType;
@@ -24927,7 +24948,7 @@ namespace ts {
2492724948
valueType.flags & TypeFlags.Null ?
2492824949
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
2492924950
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
24930-
return type.flags & TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts);
24951+
return getIntersectionWithFacts(type, facts);
2493124952
}
2493224953
if (assumeTrue) {
2493324954
const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ?
@@ -24956,17 +24977,11 @@ namespace ts {
2495624977
if (type.flags & TypeFlags.Any && literal.text === "function") {
2495724978
return type;
2495824979
}
24959-
if (assumeTrue && type.flags & TypeFlags.Unknown && literal.text === "object") {
24960-
// The non-null unknown type is used to track whether a previous narrowing operation has removed the null type
24961-
// from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null
24962-
// unknown type, and then narrows that to the non-primitive type.
24963-
return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]);
24964-
}
2496524980
const facts = assumeTrue ?
2496624981
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
2496724982
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
2496824983
const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text);
24969-
return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts);
24984+
return getTypeWithFacts(assumeTrue && impliedType ? narrowTypeByImpliedType(type, impliedType) : type, facts);
2497024985
}
2497124986

2497224987
function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) {
@@ -25019,10 +25034,10 @@ namespace ts {
2501925034

2502025035
function getImpliedTypeFromTypeofGuard(type: Type, text: string) {
2502125036
switch (text) {
25037+
case "object":
25038+
return type.flags & TypeFlags.Any ? type : getUnionType([nullType, nonPrimitiveType]);
2502225039
case "function":
2502325040
return type.flags & TypeFlags.Any ? type : globalFunctionType;
25024-
case "object":
25025-
return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type;
2502625041
default:
2502725042
return typeofTypesByName.get(text);
2502825043
}
@@ -25034,22 +25049,24 @@ namespace ts {
2503425049
// the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not
2503525050
// the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to
2503625051
// filtering by type-facts.
25037-
function narrowUnionMemberByTypeof(candidate: Type) {
25038-
return (type: Type) => {
25039-
if (isTypeSubtypeOf(type, candidate)) {
25040-
return type;
25041-
}
25042-
if (isTypeSubtypeOf(candidate, type)) {
25043-
return candidate;
25052+
function narrowTypeByImpliedType(type: Type, candidate: Type) {
25053+
if (type.flags & TypeFlags.AnyOrUnknown) {
25054+
return candidate;
25055+
}
25056+
return mapType(type, t => {
25057+
if (isTypeRelatedTo(t, candidate, strictSubtypeRelation)) {
25058+
return t;
2504425059
}
25045-
if (type.flags & TypeFlags.Instantiable) {
25046-
const constraint = getBaseConstraintOfType(type) || anyType;
25047-
if (isTypeSubtypeOf(candidate, constraint)) {
25048-
return getIntersectionType([type, candidate]);
25060+
return mapType(candidate, c => {
25061+
if (!areTypesComparable(t, c)) {
25062+
return neverType;
2504925063
}
25050-
}
25051-
return type;
25052-
};
25064+
if ((c.flags & TypeFlags.Primitive || c === globalFunctionType) && t.flags & TypeFlags.Object && !isEmptyAnonymousObjectType(t)) {
25065+
return isTypeSubtypeOf(c, t) ? c : neverType;
25066+
}
25067+
return getIntersectionType([t, c]);
25068+
});
25069+
});
2505325070
}
2505425071

2505525072
function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
@@ -25109,7 +25126,7 @@ namespace ts {
2510925126
because it is caught in the first clause.
2511025127
*/
2511125128
const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts);
25112-
return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts);
25129+
return getTypeWithFacts(narrowTypeByImpliedType(type, impliedType), switchFacts);
2511325130
}
2511425131

2511525132
function isMatchingConstructorReference(expr: Expression) {

0 commit comments

Comments
 (0)