Skip to content

Commit fde9c7f

Browse files
authored
Narrowing from truthy unknown to object (microsoft#37507)
* For x && typeof x === 'object', narrow x to just type object * Add tests * Allow arbitrary nesting / add comments * Add additional tests
1 parent e3ec7b1 commit fde9c7f

File tree

6 files changed

+379
-3
lines changed

6 files changed

+379
-3
lines changed

src/compiler/checker.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -19013,6 +19013,13 @@ namespace ts {
1901319013
return false;
1901419014
}
1901519015

19016+
// Given a source x, check if target matches x or is an && operation with an operand that matches x.
19017+
function containsTruthyCheck(source: Node, target: Node): boolean {
19018+
return isMatchingReference(source, target) ||
19019+
(target.kind === SyntaxKind.BinaryExpression && (<BinaryExpression>target).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken &&
19020+
(containsTruthyCheck(source, (<BinaryExpression>target).left) || containsTruthyCheck(source, (<BinaryExpression>target).right)));
19021+
}
19022+
1901619023
function getAccessedPropertyName(access: AccessExpression): __String | undefined {
1901719024
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
1901819025
isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
@@ -20409,15 +20416,23 @@ namespace ts {
2040920416
if (type.flags & TypeFlags.Any && literal.text === "function") {
2041020417
return type;
2041120418
}
20419+
if (assumeTrue && type.flags & TypeFlags.Unknown && literal.text === "object") {
20420+
// The pattern x && typeof x === 'object', where x is of type unknown, narrows x to type object. We don't
20421+
// need to check for the reverse typeof x === 'object' && x since that already narrows correctly.
20422+
if (typeOfExpr.parent.parent.kind === SyntaxKind.BinaryExpression) {
20423+
const expr = <BinaryExpression>typeOfExpr.parent.parent;
20424+
if (expr.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && expr.right === typeOfExpr.parent && containsTruthyCheck(reference, expr.left)) {
20425+
return nonPrimitiveType;
20426+
}
20427+
}
20428+
return getUnionType([nonPrimitiveType, nullType]);
20429+
}
2041220430
const facts = assumeTrue ?
2041320431
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
2041420432
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
2041520433
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
2041620434

2041720435
function narrowTypeForTypeof(type: Type) {
20418-
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
20419-
return getUnionType([nonPrimitiveType, nullType]);
20420-
}
2042120436
// We narrow a non-union type to an exact primitive type if the non-union type
2042220437
// is a supertype of that primitive type. For example, type 'any' can be narrowed
2042320438
// to one of the primitive types.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
tests/cases/compiler/narrowingTruthyObject.ts(3,9): error TS2531: Object is possibly 'null'.
2+
3+
4+
==== tests/cases/compiler/narrowingTruthyObject.ts (1 errors) ====
5+
function foo(x: unknown, b: boolean) {
6+
if (typeof x === 'object') {
7+
x.toString();
8+
~
9+
!!! error TS2531: Object is possibly 'null'.
10+
}
11+
if (typeof x === 'object' && x) {
12+
x.toString();
13+
}
14+
if (x && typeof x === 'object') {
15+
x.toString();
16+
}
17+
if (b && x && typeof x === 'object') {
18+
x.toString();
19+
}
20+
if (x && b && typeof x === 'object') {
21+
x.toString();
22+
}
23+
if (x && b && b && typeof x === 'object') {
24+
x.toString();
25+
}
26+
if (b && b && x && b && b && typeof x === 'object') {
27+
x.toString();
28+
}
29+
}
30+
31+
// Repro from #36870
32+
33+
function f1(x: unknown): any {
34+
return x && typeof x === 'object' && x.hasOwnProperty('x');
35+
}
36+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//// [narrowingTruthyObject.ts]
2+
function foo(x: unknown, b: boolean) {
3+
if (typeof x === 'object') {
4+
x.toString();
5+
}
6+
if (typeof x === 'object' && x) {
7+
x.toString();
8+
}
9+
if (x && typeof x === 'object') {
10+
x.toString();
11+
}
12+
if (b && x && typeof x === 'object') {
13+
x.toString();
14+
}
15+
if (x && b && typeof x === 'object') {
16+
x.toString();
17+
}
18+
if (x && b && b && typeof x === 'object') {
19+
x.toString();
20+
}
21+
if (b && b && x && b && b && typeof x === 'object') {
22+
x.toString();
23+
}
24+
}
25+
26+
// Repro from #36870
27+
28+
function f1(x: unknown): any {
29+
return x && typeof x === 'object' && x.hasOwnProperty('x');
30+
}
31+
32+
33+
//// [narrowingTruthyObject.js]
34+
"use strict";
35+
function foo(x, b) {
36+
if (typeof x === 'object') {
37+
x.toString();
38+
}
39+
if (typeof x === 'object' && x) {
40+
x.toString();
41+
}
42+
if (x && typeof x === 'object') {
43+
x.toString();
44+
}
45+
if (b && x && typeof x === 'object') {
46+
x.toString();
47+
}
48+
if (x && b && typeof x === 'object') {
49+
x.toString();
50+
}
51+
if (x && b && b && typeof x === 'object') {
52+
x.toString();
53+
}
54+
if (b && b && x && b && b && typeof x === 'object') {
55+
x.toString();
56+
}
57+
}
58+
// Repro from #36870
59+
function f1(x) {
60+
return x && typeof x === 'object' && x.hasOwnProperty('x');
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
=== tests/cases/compiler/narrowingTruthyObject.ts ===
2+
function foo(x: unknown, b: boolean) {
3+
>foo : Symbol(foo, Decl(narrowingTruthyObject.ts, 0, 0))
4+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
5+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
6+
7+
if (typeof x === 'object') {
8+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
9+
10+
x.toString();
11+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
12+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
13+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
14+
}
15+
if (typeof x === 'object' && x) {
16+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
17+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
18+
19+
x.toString();
20+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
21+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
22+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
23+
}
24+
if (x && typeof x === 'object') {
25+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
26+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
27+
28+
x.toString();
29+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
30+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
31+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
32+
}
33+
if (b && x && typeof x === 'object') {
34+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
35+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
36+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
37+
38+
x.toString();
39+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
40+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
41+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
42+
}
43+
if (x && b && typeof x === 'object') {
44+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
45+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
46+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
47+
48+
x.toString();
49+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
50+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
51+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
52+
}
53+
if (x && b && b && typeof x === 'object') {
54+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
55+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
56+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
57+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
58+
59+
x.toString();
60+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
61+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
62+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
63+
}
64+
if (b && b && x && b && b && typeof x === 'object') {
65+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
66+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
67+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
68+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
69+
>b : Symbol(b, Decl(narrowingTruthyObject.ts, 0, 24))
70+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
71+
72+
x.toString();
73+
>x.toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
74+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 0, 13))
75+
>toString : Symbol(Object.toString, Decl(lib.es5.d.ts, --, --))
76+
}
77+
}
78+
79+
// Repro from #36870
80+
81+
function f1(x: unknown): any {
82+
>f1 : Symbol(f1, Decl(narrowingTruthyObject.ts, 22, 1))
83+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
84+
85+
return x && typeof x === 'object' && x.hasOwnProperty('x');
86+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
87+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
88+
>x.hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
89+
>x : Symbol(x, Decl(narrowingTruthyObject.ts, 26, 12))
90+
>hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
91+
}
92+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
=== tests/cases/compiler/narrowingTruthyObject.ts ===
2+
function foo(x: unknown, b: boolean) {
3+
>foo : (x: unknown, b: boolean) => void
4+
>x : unknown
5+
>b : boolean
6+
7+
if (typeof x === 'object') {
8+
>typeof x === 'object' : boolean
9+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
10+
>x : unknown
11+
>'object' : "object"
12+
13+
x.toString();
14+
>x.toString() : string
15+
>x.toString : () => string
16+
>x : object | null
17+
>toString : () => string
18+
}
19+
if (typeof x === 'object' && x) {
20+
>typeof x === 'object' && x : false | object | null
21+
>typeof x === 'object' : boolean
22+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
23+
>x : unknown
24+
>'object' : "object"
25+
>x : object | null
26+
27+
x.toString();
28+
>x.toString() : string
29+
>x.toString : () => string
30+
>x : object
31+
>toString : () => string
32+
}
33+
if (x && typeof x === 'object') {
34+
>x && typeof x === 'object' : boolean
35+
>x : unknown
36+
>typeof x === 'object' : boolean
37+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
38+
>x : unknown
39+
>'object' : "object"
40+
41+
x.toString();
42+
>x.toString() : string
43+
>x.toString : () => string
44+
>x : object
45+
>toString : () => string
46+
}
47+
if (b && x && typeof x === 'object') {
48+
>b && x && typeof x === 'object' : boolean
49+
>b && x : unknown
50+
>b : boolean
51+
>x : unknown
52+
>typeof x === 'object' : boolean
53+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
54+
>x : unknown
55+
>'object' : "object"
56+
57+
x.toString();
58+
>x.toString() : string
59+
>x.toString : () => string
60+
>x : object
61+
>toString : () => string
62+
}
63+
if (x && b && typeof x === 'object') {
64+
>x && b && typeof x === 'object' : boolean
65+
>x && b : boolean
66+
>x : unknown
67+
>b : boolean
68+
>typeof x === 'object' : boolean
69+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
70+
>x : unknown
71+
>'object' : "object"
72+
73+
x.toString();
74+
>x.toString() : string
75+
>x.toString : () => string
76+
>x : object
77+
>toString : () => string
78+
}
79+
if (x && b && b && typeof x === 'object') {
80+
>x && b && b && typeof x === 'object' : boolean
81+
>x && b && b : boolean
82+
>x && b : boolean
83+
>x : unknown
84+
>b : boolean
85+
>b : true
86+
>typeof x === 'object' : boolean
87+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
88+
>x : unknown
89+
>'object' : "object"
90+
91+
x.toString();
92+
>x.toString() : string
93+
>x.toString : () => string
94+
>x : object
95+
>toString : () => string
96+
}
97+
if (b && b && x && b && b && typeof x === 'object') {
98+
>b && b && x && b && b && typeof x === 'object' : boolean
99+
>b && b && x && b && b : true
100+
>b && b && x && b : true
101+
>b && b && x : unknown
102+
>b && b : boolean
103+
>b : boolean
104+
>b : true
105+
>x : unknown
106+
>b : true
107+
>b : true
108+
>typeof x === 'object' : boolean
109+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
110+
>x : unknown
111+
>'object' : "object"
112+
113+
x.toString();
114+
>x.toString() : string
115+
>x.toString : () => string
116+
>x : object
117+
>toString : () => string
118+
}
119+
}
120+
121+
// Repro from #36870
122+
123+
function f1(x: unknown): any {
124+
>f1 : (x: unknown) => any
125+
>x : unknown
126+
127+
return x && typeof x === 'object' && x.hasOwnProperty('x');
128+
>x && typeof x === 'object' && x.hasOwnProperty('x') : boolean
129+
>x && typeof x === 'object' : boolean
130+
>x : unknown
131+
>typeof x === 'object' : boolean
132+
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
133+
>x : unknown
134+
>'object' : "object"
135+
>x.hasOwnProperty('x') : boolean
136+
>x.hasOwnProperty : (v: string | number | symbol) => boolean
137+
>x : object
138+
>hasOwnProperty : (v: string | number | symbol) => boolean
139+
>'x' : "x"
140+
}
141+

0 commit comments

Comments
 (0)