Skip to content

Commit c07f512

Browse files
authored
Fix narrowing by typeof applied to discriminant property (#51720)
* Fix narrowing by typeof applied to discriminant property * Include effects of getReferenceCandidate * Add tests
1 parent 048029e commit c07f512

File tree

5 files changed

+362
-4
lines changed

5 files changed

+362
-4
lines changed

src/compiler/checker.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26484,13 +26484,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2648426484
}
2648526485
const target = getReferenceCandidate(typeOfExpr.expression);
2648626486
if (!isMatchingReference(reference, target)) {
26487-
const propertyAccess = getDiscriminantPropertyAccess(typeOfExpr.expression, type);
26487+
if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) {
26488+
type = getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
26489+
}
26490+
const propertyAccess = getDiscriminantPropertyAccess(target, type);
2648826491
if (propertyAccess) {
2648926492
return narrowTypeByDiscriminant(type, propertyAccess, t => narrowTypeByLiteralExpression(t, literal, assumeTrue));
2649026493
}
26491-
if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) {
26492-
return getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
26493-
}
2649426494
return type;
2649526495
}
2649626496
return narrowTypeByLiteralExpression(type, literal, assumeTrue);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//// [narrowingTypeofDiscriminant.ts]
2+
function f1(obj: { kind: 'a', data: string } | { kind: 1, data: number }) {
3+
if (typeof obj.kind === "string") {
4+
obj; // { kind: 'a', data: string }
5+
}
6+
else {
7+
obj; // { kind: 1, data: number }
8+
}
9+
}
10+
11+
function f2(obj: { kind: 'a', data: string } | { kind: 1, data: number } | undefined) {
12+
if (typeof obj?.kind === "string") {
13+
obj; // { kind: 'a', data: string }
14+
}
15+
else {
16+
obj; // { kind: 1, data: number } | undefined
17+
}
18+
}
19+
20+
// Repro from #51700
21+
22+
type WrappedStringOr<T> = { value?: string } | { value?: T };
23+
24+
function numberOk(wrapped: WrappedStringOr<number> | null) {
25+
if (typeof wrapped?.value !== 'string') {
26+
return null;
27+
}
28+
return wrapped.value;
29+
}
30+
31+
function booleanBad(wrapped: WrappedStringOr<boolean> | null) {
32+
if (typeof wrapped?.value !== 'string') {
33+
return null;
34+
}
35+
return wrapped.value;
36+
}
37+
38+
function booleanFixed(wrapped: WrappedStringOr<boolean> | null) {
39+
if (typeof (wrapped?.value) !== 'string') {
40+
return null;
41+
}
42+
return wrapped.value;
43+
}
44+
45+
46+
//// [narrowingTypeofDiscriminant.js]
47+
"use strict";
48+
function f1(obj) {
49+
if (typeof obj.kind === "string") {
50+
obj; // { kind: 'a', data: string }
51+
}
52+
else {
53+
obj; // { kind: 1, data: number }
54+
}
55+
}
56+
function f2(obj) {
57+
if (typeof (obj === null || obj === void 0 ? void 0 : obj.kind) === "string") {
58+
obj; // { kind: 'a', data: string }
59+
}
60+
else {
61+
obj; // { kind: 1, data: number } | undefined
62+
}
63+
}
64+
function numberOk(wrapped) {
65+
if (typeof (wrapped === null || wrapped === void 0 ? void 0 : wrapped.value) !== 'string') {
66+
return null;
67+
}
68+
return wrapped.value;
69+
}
70+
function booleanBad(wrapped) {
71+
if (typeof (wrapped === null || wrapped === void 0 ? void 0 : wrapped.value) !== 'string') {
72+
return null;
73+
}
74+
return wrapped.value;
75+
}
76+
function booleanFixed(wrapped) {
77+
if (typeof (wrapped === null || wrapped === void 0 ? void 0 : wrapped.value) !== 'string') {
78+
return null;
79+
}
80+
return wrapped.value;
81+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
=== tests/cases/compiler/narrowingTypeofDiscriminant.ts ===
2+
function f1(obj: { kind: 'a', data: string } | { kind: 1, data: number }) {
3+
>f1 : Symbol(f1, Decl(narrowingTypeofDiscriminant.ts, 0, 0))
4+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 0, 12))
5+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 0, 18))
6+
>data : Symbol(data, Decl(narrowingTypeofDiscriminant.ts, 0, 29))
7+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 0, 48))
8+
>data : Symbol(data, Decl(narrowingTypeofDiscriminant.ts, 0, 57))
9+
10+
if (typeof obj.kind === "string") {
11+
>obj.kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 0, 18), Decl(narrowingTypeofDiscriminant.ts, 0, 48))
12+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 0, 12))
13+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 0, 18), Decl(narrowingTypeofDiscriminant.ts, 0, 48))
14+
15+
obj; // { kind: 'a', data: string }
16+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 0, 12))
17+
}
18+
else {
19+
obj; // { kind: 1, data: number }
20+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 0, 12))
21+
}
22+
}
23+
24+
function f2(obj: { kind: 'a', data: string } | { kind: 1, data: number } | undefined) {
25+
>f2 : Symbol(f2, Decl(narrowingTypeofDiscriminant.ts, 7, 1))
26+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 9, 12))
27+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 9, 18))
28+
>data : Symbol(data, Decl(narrowingTypeofDiscriminant.ts, 9, 29))
29+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 9, 48))
30+
>data : Symbol(data, Decl(narrowingTypeofDiscriminant.ts, 9, 57))
31+
32+
if (typeof obj?.kind === "string") {
33+
>obj?.kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 9, 18), Decl(narrowingTypeofDiscriminant.ts, 9, 48))
34+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 9, 12))
35+
>kind : Symbol(kind, Decl(narrowingTypeofDiscriminant.ts, 9, 18), Decl(narrowingTypeofDiscriminant.ts, 9, 48))
36+
37+
obj; // { kind: 'a', data: string }
38+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 9, 12))
39+
}
40+
else {
41+
obj; // { kind: 1, data: number } | undefined
42+
>obj : Symbol(obj, Decl(narrowingTypeofDiscriminant.ts, 9, 12))
43+
}
44+
}
45+
46+
// Repro from #51700
47+
48+
type WrappedStringOr<T> = { value?: string } | { value?: T };
49+
>WrappedStringOr : Symbol(WrappedStringOr, Decl(narrowingTypeofDiscriminant.ts, 16, 1))
50+
>T : Symbol(T, Decl(narrowingTypeofDiscriminant.ts, 20, 21))
51+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27))
52+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 48))
53+
>T : Symbol(T, Decl(narrowingTypeofDiscriminant.ts, 20, 21))
54+
55+
function numberOk(wrapped: WrappedStringOr<number> | null) {
56+
>numberOk : Symbol(numberOk, Decl(narrowingTypeofDiscriminant.ts, 20, 61))
57+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 22, 18))
58+
>WrappedStringOr : Symbol(WrappedStringOr, Decl(narrowingTypeofDiscriminant.ts, 16, 1))
59+
60+
if (typeof wrapped?.value !== 'string') {
61+
>wrapped?.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
62+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 22, 18))
63+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
64+
65+
return null;
66+
}
67+
return wrapped.value;
68+
>wrapped.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
69+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 22, 18))
70+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
71+
}
72+
73+
function booleanBad(wrapped: WrappedStringOr<boolean> | null) {
74+
>booleanBad : Symbol(booleanBad, Decl(narrowingTypeofDiscriminant.ts, 27, 1))
75+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 29, 20))
76+
>WrappedStringOr : Symbol(WrappedStringOr, Decl(narrowingTypeofDiscriminant.ts, 16, 1))
77+
78+
if (typeof wrapped?.value !== 'string') {
79+
>wrapped?.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
80+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 29, 20))
81+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
82+
83+
return null;
84+
}
85+
return wrapped.value;
86+
>wrapped.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27))
87+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 29, 20))
88+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27))
89+
}
90+
91+
function booleanFixed(wrapped: WrappedStringOr<boolean> | null) {
92+
>booleanFixed : Symbol(booleanFixed, Decl(narrowingTypeofDiscriminant.ts, 34, 1))
93+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 36, 22))
94+
>WrappedStringOr : Symbol(WrappedStringOr, Decl(narrowingTypeofDiscriminant.ts, 16, 1))
95+
96+
if (typeof (wrapped?.value) !== 'string') {
97+
>wrapped?.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
98+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 36, 22))
99+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27), Decl(narrowingTypeofDiscriminant.ts, 20, 48))
100+
101+
return null;
102+
}
103+
return wrapped.value;
104+
>wrapped.value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27))
105+
>wrapped : Symbol(wrapped, Decl(narrowingTypeofDiscriminant.ts, 36, 22))
106+
>value : Symbol(value, Decl(narrowingTypeofDiscriminant.ts, 20, 27))
107+
}
108+
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
=== tests/cases/compiler/narrowingTypeofDiscriminant.ts ===
2+
function f1(obj: { kind: 'a', data: string } | { kind: 1, data: number }) {
3+
>f1 : (obj: { kind: 'a'; data: string;} | { kind: 1; data: number;}) => void
4+
>obj : { kind: 'a'; data: string; } | { kind: 1; data: number; }
5+
>kind : "a"
6+
>data : string
7+
>kind : 1
8+
>data : number
9+
10+
if (typeof obj.kind === "string") {
11+
>typeof obj.kind === "string" : boolean
12+
>typeof obj.kind : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
13+
>obj.kind : "a" | 1
14+
>obj : { kind: "a"; data: string; } | { kind: 1; data: number; }
15+
>kind : "a" | 1
16+
>"string" : "string"
17+
18+
obj; // { kind: 'a', data: string }
19+
>obj : { kind: "a"; data: string; }
20+
}
21+
else {
22+
obj; // { kind: 1, data: number }
23+
>obj : { kind: 1; data: number; }
24+
}
25+
}
26+
27+
function f2(obj: { kind: 'a', data: string } | { kind: 1, data: number } | undefined) {
28+
>f2 : (obj: { kind: 'a'; data: string;} | { kind: 1; data: number;} | undefined) => void
29+
>obj : { kind: 'a'; data: string; } | { kind: 1; data: number; } | undefined
30+
>kind : "a"
31+
>data : string
32+
>kind : 1
33+
>data : number
34+
35+
if (typeof obj?.kind === "string") {
36+
>typeof obj?.kind === "string" : boolean
37+
>typeof obj?.kind : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
38+
>obj?.kind : "a" | 1 | undefined
39+
>obj : { kind: "a"; data: string; } | { kind: 1; data: number; } | undefined
40+
>kind : "a" | 1 | undefined
41+
>"string" : "string"
42+
43+
obj; // { kind: 'a', data: string }
44+
>obj : { kind: "a"; data: string; }
45+
}
46+
else {
47+
obj; // { kind: 1, data: number } | undefined
48+
>obj : { kind: 1; data: number; } | undefined
49+
}
50+
}
51+
52+
// Repro from #51700
53+
54+
type WrappedStringOr<T> = { value?: string } | { value?: T };
55+
>WrappedStringOr : WrappedStringOr<T>
56+
>value : string | undefined
57+
>value : T | undefined
58+
59+
function numberOk(wrapped: WrappedStringOr<number> | null) {
60+
>numberOk : (wrapped: WrappedStringOr<number> | null) => string | null
61+
>wrapped : WrappedStringOr<number> | null
62+
>null : null
63+
64+
if (typeof wrapped?.value !== 'string') {
65+
>typeof wrapped?.value !== 'string' : boolean
66+
>typeof wrapped?.value : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
67+
>wrapped?.value : string | number | undefined
68+
>wrapped : WrappedStringOr<number> | null
69+
>value : string | number | undefined
70+
>'string' : "string"
71+
72+
return null;
73+
>null : null
74+
}
75+
return wrapped.value;
76+
>wrapped.value : string
77+
>wrapped : WrappedStringOr<number>
78+
>value : string
79+
}
80+
81+
function booleanBad(wrapped: WrappedStringOr<boolean> | null) {
82+
>booleanBad : (wrapped: WrappedStringOr<boolean> | null) => string | null
83+
>wrapped : WrappedStringOr<boolean> | null
84+
>null : null
85+
86+
if (typeof wrapped?.value !== 'string') {
87+
>typeof wrapped?.value !== 'string' : boolean
88+
>typeof wrapped?.value : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
89+
>wrapped?.value : string | boolean | undefined
90+
>wrapped : WrappedStringOr<boolean> | null
91+
>value : string | boolean | undefined
92+
>'string' : "string"
93+
94+
return null;
95+
>null : null
96+
}
97+
return wrapped.value;
98+
>wrapped.value : string
99+
>wrapped : { value?: string | undefined; }
100+
>value : string
101+
}
102+
103+
function booleanFixed(wrapped: WrappedStringOr<boolean> | null) {
104+
>booleanFixed : (wrapped: WrappedStringOr<boolean> | null) => string | null
105+
>wrapped : WrappedStringOr<boolean> | null
106+
>null : null
107+
108+
if (typeof (wrapped?.value) !== 'string') {
109+
>typeof (wrapped?.value) !== 'string' : boolean
110+
>typeof (wrapped?.value) : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
111+
>(wrapped?.value) : string | boolean | undefined
112+
>wrapped?.value : string | boolean | undefined
113+
>wrapped : WrappedStringOr<boolean> | null
114+
>value : string | boolean | undefined
115+
>'string' : "string"
116+
117+
return null;
118+
>null : null
119+
}
120+
return wrapped.value;
121+
>wrapped.value : string
122+
>wrapped : { value?: string | undefined; }
123+
>value : string
124+
}
125+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// @strict: true
2+
3+
function f1(obj: { kind: 'a', data: string } | { kind: 1, data: number }) {
4+
if (typeof obj.kind === "string") {
5+
obj; // { kind: 'a', data: string }
6+
}
7+
else {
8+
obj; // { kind: 1, data: number }
9+
}
10+
}
11+
12+
function f2(obj: { kind: 'a', data: string } | { kind: 1, data: number } | undefined) {
13+
if (typeof obj?.kind === "string") {
14+
obj; // { kind: 'a', data: string }
15+
}
16+
else {
17+
obj; // { kind: 1, data: number } | undefined
18+
}
19+
}
20+
21+
// Repro from #51700
22+
23+
type WrappedStringOr<T> = { value?: string } | { value?: T };
24+
25+
function numberOk(wrapped: WrappedStringOr<number> | null) {
26+
if (typeof wrapped?.value !== 'string') {
27+
return null;
28+
}
29+
return wrapped.value;
30+
}
31+
32+
function booleanBad(wrapped: WrappedStringOr<boolean> | null) {
33+
if (typeof wrapped?.value !== 'string') {
34+
return null;
35+
}
36+
return wrapped.value;
37+
}
38+
39+
function booleanFixed(wrapped: WrappedStringOr<boolean> | null) {
40+
if (typeof (wrapped?.value) !== 'string') {
41+
return null;
42+
}
43+
return wrapped.value;
44+
}

0 commit comments

Comments
 (0)