diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b764348cc6f10..2d86add4ee7df 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -753,6 +753,7 @@ namespace ts { const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType); const intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic"); const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown"); + const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown"); const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined"); const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType); const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined"); @@ -13995,7 +13996,9 @@ namespace ts { const includes = addTypesToUnion(typeSet, 0, types); if (unionReduction !== UnionReduction.None) { if (includes & TypeFlags.AnyOrUnknown) { - return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType; + return includes & TypeFlags.Any ? + includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : + includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType; } if (exactOptionalPropertyTypes && includes & TypeFlags.Undefined) { const missingIndex = binarySearch(typeSet, missingType, getTypeId, compareValues); @@ -22100,13 +22103,6 @@ namespace ts { return false; } - // Given a source x, check if target matches x or is an && operation with an operand that matches x. - function containsTruthyCheck(source: Node, target: Node): boolean { - return isMatchingReference(source, target) || - (target.kind === SyntaxKind.BinaryExpression && (target as BinaryExpression).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && - (containsTruthyCheck(source, (target as BinaryExpression).left) || containsTruthyCheck(source, (target as BinaryExpression).right))); - } - function getPropertyAccess(expr: Expression) { if (isAccessExpression(expr)) { return expr; @@ -23119,7 +23115,8 @@ namespace ts { if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === SyntaxKind.NonNullExpression && !(resultType.flags & TypeFlags.Never) && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) { return declaredType; } - return resultType; + // The non-null unknown type should never escape control flow analysis. + return resultType === nonNullUnknownType ? unknownType : resultType; function getOrSetCacheKey() { if (isKeySet) { @@ -23607,7 +23604,8 @@ namespace ts { function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type { if (isMatchingReference(reference, expr)) { - return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); + return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType : + getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); } if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); @@ -23765,7 +23763,7 @@ namespace ts { valueType.flags & TypeFlags.Null ? assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; - return getTypeWithFacts(type, facts); + return type.flags & TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts); } if (assumeTrue) { const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ? @@ -23795,15 +23793,10 @@ namespace ts { return type; } if (assumeTrue && type.flags & TypeFlags.Unknown && literal.text === "object") { - // The pattern x && typeof x === 'object', where x is of type unknown, narrows x to type object. We don't - // need to check for the reverse typeof x === 'object' && x since that already narrows correctly. - if (typeOfExpr.parent.parent.kind === SyntaxKind.BinaryExpression) { - const expr = typeOfExpr.parent.parent as BinaryExpression; - if (expr.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && expr.right === typeOfExpr.parent && containsTruthyCheck(reference, expr.left)) { - return nonPrimitiveType; - } - } - return getUnionType([nonPrimitiveType, nullType]); + // The non-null unknown type is used to track whether a previous narrowing operation has removed the null type + // from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null + // unknown type, and then narrows that to the non-primitive type. + return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]); } const facts = assumeTrue ? typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : diff --git a/tests/baselines/reference/controlFlowTypeofObject.errors.txt b/tests/baselines/reference/controlFlowTypeofObject.errors.txt new file mode 100644 index 0000000000000..dc6a46696ccf5 --- /dev/null +++ b/tests/baselines/reference/controlFlowTypeofObject.errors.txt @@ -0,0 +1,77 @@ +tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts(66,13): error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'. + Type 'null' is not assignable to type 'object'. + + +==== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts (1 errors) ==== + declare function obj(x: object): void; + + function f1(x: unknown) { + if (!x) { + return; + } + if (typeof x === 'object') { + obj(x); + } + } + + function f2(x: unknown) { + if (x === null) { + return; + } + if (typeof x === 'object') { + obj(x); + } + } + + function f3(x: unknown) { + if (x == null) { + return; + } + if (typeof x === 'object') { + obj(x); + } + } + + function f4(x: unknown) { + if (x == undefined) { + return; + } + if (typeof x === 'object') { + obj(x); + } + } + + function f5(x: unknown) { + if (!!true) { + if (!x) { + return; + } + } + else { + if (x === null) { + return; + } + } + if (typeof x === 'object') { + obj(x); + } + } + + function f6(x: unknown) { + if (x === null) { + x; + } + else { + x; + if (typeof x === 'object') { + obj(x); + } + } + if (typeof x === 'object') { + obj(x); // Error + ~ +!!! error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'. +!!! error TS2345: Type 'null' is not assignable to type 'object'. + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/controlFlowTypeofObject.js b/tests/baselines/reference/controlFlowTypeofObject.js new file mode 100644 index 0000000000000..3c87da5daa056 --- /dev/null +++ b/tests/baselines/reference/controlFlowTypeofObject.js @@ -0,0 +1,144 @@ +//// [controlFlowTypeofObject.ts] +declare function obj(x: object): void; + +function f1(x: unknown) { + if (!x) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f2(x: unknown) { + if (x === null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f3(x: unknown) { + if (x == null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f4(x: unknown) { + if (x == undefined) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f5(x: unknown) { + if (!!true) { + if (!x) { + return; + } + } + else { + if (x === null) { + return; + } + } + if (typeof x === 'object') { + obj(x); + } +} + +function f6(x: unknown) { + if (x === null) { + x; + } + else { + x; + if (typeof x === 'object') { + obj(x); + } + } + if (typeof x === 'object') { + obj(x); // Error + } +} + + +//// [controlFlowTypeofObject.js] +"use strict"; +function f1(x) { + if (!x) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} +function f2(x) { + if (x === null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} +function f3(x) { + if (x == null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} +function f4(x) { + if (x == undefined) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} +function f5(x) { + if (!!true) { + if (!x) { + return; + } + } + else { + if (x === null) { + return; + } + } + if (typeof x === 'object') { + obj(x); + } +} +function f6(x) { + if (x === null) { + x; + } + else { + x; + if (typeof x === 'object') { + obj(x); + } + } + if (typeof x === 'object') { + obj(x); // Error + } +} + + +//// [controlFlowTypeofObject.d.ts] +declare function obj(x: object): void; +declare function f1(x: unknown): void; +declare function f2(x: unknown): void; +declare function f3(x: unknown): void; +declare function f4(x: unknown): void; +declare function f5(x: unknown): void; +declare function f6(x: unknown): void; diff --git a/tests/baselines/reference/controlFlowTypeofObject.symbols b/tests/baselines/reference/controlFlowTypeofObject.symbols new file mode 100644 index 0000000000000..241381b50c064 --- /dev/null +++ b/tests/baselines/reference/controlFlowTypeofObject.symbols @@ -0,0 +1,136 @@ +=== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts === +declare function obj(x: object): void; +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 0, 21)) + +function f1(x: unknown) { +>f1 : Symbol(f1, Decl(controlFlowTypeofObject.ts, 0, 38)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12)) + + if (!x) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12)) + + return; + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12)) + } +} + +function f2(x: unknown) { +>f2 : Symbol(f2, Decl(controlFlowTypeofObject.ts, 9, 1)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12)) + + if (x === null) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12)) + + return; + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12)) + } +} + +function f3(x: unknown) { +>f3 : Symbol(f3, Decl(controlFlowTypeofObject.ts, 18, 1)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12)) + + if (x == null) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12)) + + return; + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12)) + } +} + +function f4(x: unknown) { +>f4 : Symbol(f4, Decl(controlFlowTypeofObject.ts, 27, 1)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12)) + + if (x == undefined) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12)) +>undefined : Symbol(undefined) + + return; + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12)) + } +} + +function f5(x: unknown) { +>f5 : Symbol(f5, Decl(controlFlowTypeofObject.ts, 36, 1)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12)) + + if (!!true) { + if (!x) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12)) + + return; + } + } + else { + if (x === null) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12)) + + return; + } + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12)) + } +} + +function f6(x: unknown) { +>f6 : Symbol(f6, Decl(controlFlowTypeofObject.ts, 52, 1)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + + if (x === null) { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + + x; +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + } + else { + x; +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + + obj(x); +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + } + } + if (typeof x === 'object') { +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + + obj(x); // Error +>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0)) +>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12)) + } +} + diff --git a/tests/baselines/reference/controlFlowTypeofObject.types b/tests/baselines/reference/controlFlowTypeofObject.types new file mode 100644 index 0000000000000..68d99e6f0c266 --- /dev/null +++ b/tests/baselines/reference/controlFlowTypeofObject.types @@ -0,0 +1,179 @@ +=== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts === +declare function obj(x: object): void; +>obj : (x: object) => void +>x : object + +function f1(x: unknown) { +>f1 : (x: unknown) => void +>x : unknown + + if (!x) { +>!x : boolean +>x : unknown + + return; + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } +} + +function f2(x: unknown) { +>f2 : (x: unknown) => void +>x : unknown + + if (x === null) { +>x === null : boolean +>x : unknown +>null : null + + return; + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } +} + +function f3(x: unknown) { +>f3 : (x: unknown) => void +>x : unknown + + if (x == null) { +>x == null : boolean +>x : unknown +>null : null + + return; + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } +} + +function f4(x: unknown) { +>f4 : (x: unknown) => void +>x : unknown + + if (x == undefined) { +>x == undefined : boolean +>x : unknown +>undefined : undefined + + return; + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } +} + +function f5(x: unknown) { +>f5 : (x: unknown) => void +>x : unknown + + if (!!true) { +>!!true : true +>!true : false +>true : true + + if (!x) { +>!x : boolean +>x : unknown + + return; + } + } + else { + if (x === null) { +>x === null : boolean +>x : unknown +>null : null + + return; + } + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } +} + +function f6(x: unknown) { +>f6 : (x: unknown) => void +>x : unknown + + if (x === null) { +>x === null : boolean +>x : unknown +>null : null + + x; +>x : null + } + else { + x; +>x : unknown + + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); +>obj(x) : void +>obj : (x: object) => void +>x : object + } + } + if (typeof x === 'object') { +>typeof x === 'object' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>'object' : "object" + + obj(x); // Error +>obj(x) : void +>obj : (x: object) => void +>x : object | null + } +} + diff --git a/tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts b/tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts new file mode 100644 index 0000000000000..db134447ed878 --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts @@ -0,0 +1,71 @@ +// @strict: true +// @declaration: true + +declare function obj(x: object): void; + +function f1(x: unknown) { + if (!x) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f2(x: unknown) { + if (x === null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f3(x: unknown) { + if (x == null) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f4(x: unknown) { + if (x == undefined) { + return; + } + if (typeof x === 'object') { + obj(x); + } +} + +function f5(x: unknown) { + if (!!true) { + if (!x) { + return; + } + } + else { + if (x === null) { + return; + } + } + if (typeof x === 'object') { + obj(x); + } +} + +function f6(x: unknown) { + if (x === null) { + x; + } + else { + x; + if (typeof x === 'object') { + obj(x); + } + } + if (typeof x === 'object') { + obj(x); // Error + } +}