Skip to content

Commit d8f736d

Browse files
authored
Change typeof narrowing to narrow selected union members (#25243)
* For typeof narrow all union members prior to filtering * Revise narrowTypeByTypeof to both narrow unions and applicable union members * Add repros from issue
1 parent c62920a commit d8f736d

9 files changed

+217
-47
lines changed

src/compiler/checker.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14963,30 +14963,34 @@ namespace ts {
1496314963
if (type.flags & TypeFlags.Any && literal.text === "function") {
1496414964
return type;
1496514965
}
14966-
if (assumeTrue && !(type.flags & TypeFlags.Union)) {
14967-
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
14968-
return getUnionType([nonPrimitiveType, nullType]);
14969-
}
14970-
// We narrow a non-union type to an exact primitive type if the non-union type
14971-
// is a supertype of that primitive type. For example, type 'any' can be narrowed
14972-
// to one of the primitive types.
14973-
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
14974-
if (targetType) {
14975-
if (isTypeSubtypeOf(targetType, type)) {
14976-
return targetType;
14977-
}
14978-
if (type.flags & TypeFlags.Instantiable) {
14979-
const constraint = getBaseConstraintOfType(type) || anyType;
14980-
if (isTypeSubtypeOf(targetType, constraint)) {
14981-
return getIntersectionType([type, targetType]);
14966+
const facts = assumeTrue ?
14967+
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
14968+
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
14969+
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
14970+
14971+
function narrowTypeForTypeof(type: Type) {
14972+
if (assumeTrue && !(type.flags & TypeFlags.Union)) {
14973+
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
14974+
return getUnionType([nonPrimitiveType, nullType]);
14975+
}
14976+
// We narrow a non-union type to an exact primitive type if the non-union type
14977+
// is a supertype of that primitive type. For example, type 'any' can be narrowed
14978+
// to one of the primitive types.
14979+
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
14980+
if (targetType) {
14981+
if (isTypeSubtypeOf(targetType, type)) {
14982+
return isTypeAny(type) ? targetType : getIntersectionType([type, targetType]); // Intersection to handle `string` being a subtype of `keyof T`
14983+
}
14984+
if (type.flags & TypeFlags.Instantiable) {
14985+
const constraint = getBaseConstraintOfType(type) || anyType;
14986+
if (isTypeSubtypeOf(targetType, constraint)) {
14987+
return getIntersectionType([type, targetType]);
14988+
}
1498214989
}
1498314990
}
1498414991
}
14992+
return type;
1498514993
}
14986-
const facts = assumeTrue ?
14987-
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
14988-
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
14989-
return getTypeWithFacts(type, facts);
1499014994
}
1499114995

1499214996
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {

tests/baselines/reference/controlFlowIfStatement.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function c<T>(data: string | T): T {
104104
>JSON.parse : (text: string, reviver?: (key: any, value: any) => any) => any
105105
>JSON : JSON
106106
>parse : (text: string, reviver?: (key: any, value: any) => any) => any
107-
>data : string
107+
>data : string | (T & string)
108108
}
109109
else {
110110
return data;

tests/baselines/reference/recursiveTypeRelations.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export function css<S extends { [K in keyof S]: string }>(styles: S, ...classNam
5858
>"string" : "string"
5959

6060
return styles[arg];
61-
>styles[arg] : S[keyof S]
61+
>styles[arg] : S[keyof S & string]
6262
>styles : S
63-
>arg : keyof S
63+
>arg : keyof S & string
6464
}
6565
if (typeof arg == "object") {
6666
>typeof arg == "object" : boolean
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//// [strictTypeofUnionNarrowing.ts]
2+
function stringify1(anything: { toString(): string } | undefined): string {
3+
return typeof anything === "string" ? anything.toUpperCase() : "";
4+
}
5+
6+
function stringify2(anything: {} | undefined): string {
7+
return typeof anything === "string" ? anything.toUpperCase() : "";
8+
}
9+
10+
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
11+
return typeof anything === "string" ? anything.toUpperCase() : "";
12+
}
13+
14+
function stringify4(anything: { toString?(): string } | undefined): string {
15+
return typeof anything === "string" ? anything.toUpperCase() : "";
16+
}
17+
18+
19+
//// [strictTypeofUnionNarrowing.js]
20+
"use strict";
21+
function stringify1(anything) {
22+
return typeof anything === "string" ? anything.toUpperCase() : "";
23+
}
24+
function stringify2(anything) {
25+
return typeof anything === "string" ? anything.toUpperCase() : "";
26+
}
27+
function stringify3(anything) {
28+
return typeof anything === "string" ? anything.toUpperCase() : "";
29+
}
30+
function stringify4(anything) {
31+
return typeof anything === "string" ? anything.toUpperCase() : "";
32+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
=== tests/cases/compiler/strictTypeofUnionNarrowing.ts ===
2+
function stringify1(anything: { toString(): string } | undefined): string {
3+
>stringify1 : Symbol(stringify1, Decl(strictTypeofUnionNarrowing.ts, 0, 0))
4+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
5+
>toString : Symbol(toString, Decl(strictTypeofUnionNarrowing.ts, 0, 31))
6+
7+
return typeof anything === "string" ? anything.toUpperCase() : "";
8+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
9+
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
10+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
11+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
12+
}
13+
14+
function stringify2(anything: {} | undefined): string {
15+
>stringify2 : Symbol(stringify2, Decl(strictTypeofUnionNarrowing.ts, 2, 1))
16+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
17+
18+
return typeof anything === "string" ? anything.toUpperCase() : "";
19+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
20+
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
21+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
22+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
23+
}
24+
25+
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
26+
>stringify3 : Symbol(stringify3, Decl(strictTypeofUnionNarrowing.ts, 6, 1))
27+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
28+
29+
return typeof anything === "string" ? anything.toUpperCase() : "";
30+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
31+
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
32+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
33+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
34+
}
35+
36+
function stringify4(anything: { toString?(): string } | undefined): string {
37+
>stringify4 : Symbol(stringify4, Decl(strictTypeofUnionNarrowing.ts, 10, 1))
38+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
39+
>toString : Symbol(toString, Decl(strictTypeofUnionNarrowing.ts, 12, 31))
40+
41+
return typeof anything === "string" ? anything.toUpperCase() : "";
42+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
43+
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
44+
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
45+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
46+
}
47+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
=== tests/cases/compiler/strictTypeofUnionNarrowing.ts ===
2+
function stringify1(anything: { toString(): string } | undefined): string {
3+
>stringify1 : (anything: { toString(): string; } | undefined) => string
4+
>anything : { toString(): string; } | undefined
5+
>toString : () => string
6+
7+
return typeof anything === "string" ? anything.toUpperCase() : "";
8+
>typeof anything === "string" ? anything.toUpperCase() : "" : string
9+
>typeof anything === "string" : boolean
10+
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
11+
>anything : { toString(): string; } | undefined
12+
>"string" : "string"
13+
>anything.toUpperCase() : string
14+
>anything.toUpperCase : () => string
15+
>anything : { toString(): string; } & string
16+
>toUpperCase : () => string
17+
>"" : ""
18+
}
19+
20+
function stringify2(anything: {} | undefined): string {
21+
>stringify2 : (anything: {} | undefined) => string
22+
>anything : {} | undefined
23+
24+
return typeof anything === "string" ? anything.toUpperCase() : "";
25+
>typeof anything === "string" ? anything.toUpperCase() : "" : string
26+
>typeof anything === "string" : boolean
27+
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
28+
>anything : {} | undefined
29+
>"string" : "string"
30+
>anything.toUpperCase() : string
31+
>anything.toUpperCase : () => string
32+
>anything : string & {}
33+
>toUpperCase : () => string
34+
>"" : ""
35+
}
36+
37+
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
38+
>stringify3 : (anything: unknown) => string
39+
>anything : unknown
40+
41+
return typeof anything === "string" ? anything.toUpperCase() : "";
42+
>typeof anything === "string" ? anything.toUpperCase() : "" : string
43+
>typeof anything === "string" : boolean
44+
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
45+
>anything : unknown
46+
>"string" : "string"
47+
>anything.toUpperCase() : string
48+
>anything.toUpperCase : () => string
49+
>anything : string
50+
>toUpperCase : () => string
51+
>"" : ""
52+
}
53+
54+
function stringify4(anything: { toString?(): string } | undefined): string {
55+
>stringify4 : (anything: {} | undefined) => string
56+
>anything : {} | undefined
57+
>toString : (() => string) | undefined
58+
59+
return typeof anything === "string" ? anything.toUpperCase() : "";
60+
>typeof anything === "string" ? anything.toUpperCase() : "" : string
61+
>typeof anything === "string" : boolean
62+
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
63+
>anything : {} | undefined
64+
>"string" : "string"
65+
>anything.toUpperCase() : string
66+
>anything.toUpperCase : () => string
67+
>anything : {} & string
68+
>toUpperCase : () => string
69+
>"" : ""
70+
}
71+

tests/baselines/reference/typeGuardOfFormTypeOfPrimitiveSubtype.types

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ if (typeof a === "number") {
1414

1515
let c: number = a;
1616
>c : number
17-
>a : number
17+
>a : number & {}
1818
}
1919
if (typeof a === "string") {
2020
>typeof a === "string" : boolean
@@ -24,7 +24,7 @@ if (typeof a === "string") {
2424

2525
let c: string = a;
2626
>c : string
27-
>a : string
27+
>a : string & {}
2828
}
2929
if (typeof a === "boolean") {
3030
>typeof a === "boolean" : boolean
@@ -34,7 +34,7 @@ if (typeof a === "boolean") {
3434

3535
let c: boolean = a;
3636
>c : boolean
37-
>a : boolean
37+
>a : (false & {}) | (true & {})
3838
}
3939

4040
if (typeof b === "number") {
@@ -45,7 +45,7 @@ if (typeof b === "number") {
4545

4646
let c: number = b;
4747
>c : number
48-
>b : number
48+
>b : { toString(): string; } & number
4949
}
5050
if (typeof b === "string") {
5151
>typeof b === "string" : boolean
@@ -55,7 +55,7 @@ if (typeof b === "string") {
5555

5656
let c: string = b;
5757
>c : string
58-
>b : string
58+
>b : { toString(): string; } & string
5959
}
6060
if (typeof b === "boolean") {
6161
>typeof b === "boolean" : boolean
@@ -65,6 +65,6 @@ if (typeof b === "boolean") {
6565

6666
let c: boolean = b;
6767
>c : boolean
68-
>b : boolean
68+
>b : ({ toString(): string; } & false) | ({ toString(): string; } & true)
6969
}
7070

0 commit comments

Comments
 (0)