mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-30 04:16:48 -05:00
Narrowing from truthy unknown to object (#37507)
* For x && typeof x === 'object', narrow x to just type object * Add tests * Allow arbitrary nesting / add comments * Add additional tests
This commit is contained in:
@@ -19013,6 +19013,13 @@ 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 && (<BinaryExpression>target).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken &&
|
||||
(containsTruthyCheck(source, (<BinaryExpression>target).left) || containsTruthyCheck(source, (<BinaryExpression>target).right)));
|
||||
}
|
||||
|
||||
function getAccessedPropertyName(access: AccessExpression): __String | undefined {
|
||||
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
|
||||
isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
|
||||
@@ -20409,15 +20416,23 @@ namespace ts {
|
||||
if (type.flags & TypeFlags.Any && literal.text === "function") {
|
||||
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 = <BinaryExpression>typeOfExpr.parent.parent;
|
||||
if (expr.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && expr.right === typeOfExpr.parent && containsTruthyCheck(reference, expr.left)) {
|
||||
return nonPrimitiveType;
|
||||
}
|
||||
}
|
||||
return getUnionType([nonPrimitiveType, nullType]);
|
||||
}
|
||||
const facts = assumeTrue ?
|
||||
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
|
||||
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
|
||||
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
|
||||
|
||||
function narrowTypeForTypeof(type: Type) {
|
||||
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
|
||||
return getUnionType([nonPrimitiveType, nullType]);
|
||||
}
|
||||
// We narrow a non-union type to an exact primitive type if the non-union type
|
||||
// is a supertype of that primitive type. For example, type 'any' can be narrowed
|
||||
// to one of the primitive types.
|
||||
|
||||
36
tests/baselines/reference/narrowingTruthyObject.errors.txt
Normal file
36
tests/baselines/reference/narrowingTruthyObject.errors.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
tests/cases/compiler/narrowingTruthyObject.ts(3,9): error TS2531: Object is possibly 'null'.
|
||||
|
||||
|
||||
==== tests/cases/compiler/narrowingTruthyObject.ts (1 errors) ====
|
||||
function foo(x: unknown, b: boolean) {
|
||||
if (typeof x === 'object') {
|
||||
x.toString();
|
||||
~
|
||||
!!! error TS2531: Object is possibly 'null'.
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
x.toString();
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Repro from #36870
|
||||
|
||||
function f1(x: unknown): any {
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
}
|
||||
|
||||
61
tests/baselines/reference/narrowingTruthyObject.js
Normal file
61
tests/baselines/reference/narrowingTruthyObject.js
Normal file
@@ -0,0 +1,61 @@
|
||||
//// [narrowingTruthyObject.ts]
|
||||
function foo(x: unknown, b: boolean) {
|
||||
if (typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
x.toString();
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Repro from #36870
|
||||
|
||||
function f1(x: unknown): any {
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
}
|
||||
|
||||
|
||||
//// [narrowingTruthyObject.js]
|
||||
"use strict";
|
||||
function foo(x, b) {
|
||||
if (typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
x.toString();
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
}
|
||||
// Repro from #36870
|
||||
function f1(x) {
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
}
|
||||
92
tests/baselines/reference/narrowingTruthyObject.symbols
Normal file
92
tests/baselines/reference/narrowingTruthyObject.symbols
Normal file
@@ -0,0 +1,92 @@
|
||||
=== tests/cases/compiler/narrowingTruthyObject.ts ===
|
||||
function foo(x: unknown, b: boolean) {
|
||||
>foo : Symbol(foo, Decl(narrowingTruthyObject.ts, 0, 0))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
|
||||
if (typeof x === 'object') {
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
|
||||
x.toString();
|
||||
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
|
||||
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
}
|
||||
|
||||
// Repro from #36870
|
||||
|
||||
function f1(x: unknown): any {
|
||||
>f1 : Symbol(f1, Decl(narrowingTruthyObject.ts, 22, 1))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
|
||||
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
|
||||
>x.hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
|
||||
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
|
||||
>hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
|
||||
}
|
||||
|
||||
141
tests/baselines/reference/narrowingTruthyObject.types
Normal file
141
tests/baselines/reference/narrowingTruthyObject.types
Normal file
@@ -0,0 +1,141 @@
|
||||
=== tests/cases/compiler/narrowingTruthyObject.ts ===
|
||||
function foo(x: unknown, b: boolean) {
|
||||
>foo : (x: unknown, b: boolean) => void
|
||||
>x : unknown
|
||||
>b : boolean
|
||||
|
||||
if (typeof x === 'object') {
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object | null
|
||||
>toString : () => string
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
>typeof x === 'object' && x : false | object | null
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
>x : object | null
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
>x && typeof x === 'object' : boolean
|
||||
>x : unknown
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
>b && x && typeof x === 'object' : boolean
|
||||
>b && x : unknown
|
||||
>b : boolean
|
||||
>x : unknown
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
>x && b && typeof x === 'object' : boolean
|
||||
>x && b : boolean
|
||||
>x : unknown
|
||||
>b : boolean
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
>x && b && b && typeof x === 'object' : boolean
|
||||
>x && b && b : boolean
|
||||
>x && b : boolean
|
||||
>x : unknown
|
||||
>b : boolean
|
||||
>b : true
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
>b && b && x && b && b && typeof x === 'object' : boolean
|
||||
>b && b && x && b && b : true
|
||||
>b && b && x && b : true
|
||||
>b && b && x : unknown
|
||||
>b && b : boolean
|
||||
>b : boolean
|
||||
>b : true
|
||||
>x : unknown
|
||||
>b : true
|
||||
>b : true
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
|
||||
x.toString();
|
||||
>x.toString() : string
|
||||
>x.toString : () => string
|
||||
>x : object
|
||||
>toString : () => string
|
||||
}
|
||||
}
|
||||
|
||||
// Repro from #36870
|
||||
|
||||
function f1(x: unknown): any {
|
||||
>f1 : (x: unknown) => any
|
||||
>x : unknown
|
||||
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
>x && typeof x === 'object' && x.hasOwnProperty('x') : boolean
|
||||
>x && typeof x === 'object' : boolean
|
||||
>x : unknown
|
||||
>typeof x === 'object' : boolean
|
||||
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
||||
>x : unknown
|
||||
>'object' : "object"
|
||||
>x.hasOwnProperty('x') : boolean
|
||||
>x.hasOwnProperty : (v: string | number | symbol) => boolean
|
||||
>x : object
|
||||
>hasOwnProperty : (v: string | number | symbol) => boolean
|
||||
>'x' : "x"
|
||||
}
|
||||
|
||||
31
tests/cases/compiler/narrowingTruthyObject.ts
Normal file
31
tests/cases/compiler/narrowingTruthyObject.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// @strict: true
|
||||
|
||||
function foo(x: unknown, b: boolean) {
|
||||
if (typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (typeof x === 'object' && x) {
|
||||
x.toString();
|
||||
}
|
||||
if (x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && x && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
if (b && b && x && b && b && typeof x === 'object') {
|
||||
x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Repro from #36870
|
||||
|
||||
function f1(x: unknown): any {
|
||||
return x && typeof x === 'object' && x.hasOwnProperty('x');
|
||||
}
|
||||
Reference in New Issue
Block a user