Skip to content

Commit 4189b4d

Browse files
committed
Narrowing type parameter intersects w/narrowed types
This makes sure that a union type that includes a type parameter is still usable as the actual type that the type guard narrows to.
1 parent 013744b commit 4189b4d

File tree

5 files changed

+156
-7
lines changed

5 files changed

+156
-7
lines changed

src/compiler/checker.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7900,13 +7900,17 @@ namespace ts {
79007900
return TypeFacts.All;
79017901
}
79027902

7903-
function getTypeWithFacts(type: Type, include: TypeFacts) {
7903+
function getTypeWithFacts(type: Type, include: TypeFacts, intersectForTypeParameters = false) {
79047904
if (!(type.flags & TypeFlags.Union)) {
79057905
return getTypeFacts(type) & include ? type : neverType;
79067906
}
79077907
let firstType: Type;
7908+
let hasTypeParameter = false;
79087909
let types: Type[];
79097910
for (const t of (type as UnionType).types) {
7911+
if (t.flags & TypeFlags.TypeParameter) {
7912+
hasTypeParameter = true;
7913+
}
79107914
if (getTypeFacts(t) & include) {
79117915
if (!firstType) {
79127916
firstType = t;
@@ -7919,7 +7923,19 @@ namespace ts {
79197923
}
79207924
}
79217925
}
7922-
return firstType ? types ? getUnionType(types) : firstType : neverType;
7926+
const narrowed = types ? getUnionType(types) :
7927+
firstType ? firstType : neverType;
7928+
// if there is a type parameter in the narrowed type,
7929+
// add an intersection with the members of the narrowed type so that the shape of the type is correct
7930+
if (type.flags & TypeFlags.Union &&
7931+
narrowed.flags & TypeFlags.Union &&
7932+
hasTypeParameter &&
7933+
intersectForTypeParameters) {
7934+
return getIntersectionType(types.concat([narrowed]));
7935+
}
7936+
else {
7937+
return narrowed;
7938+
}
79237939
}
79247940

79257941
function getTypeWithDefault(type: Type, defaultExpression: Expression) {
@@ -8183,7 +8199,7 @@ namespace ts {
81838199
let type = getTypeAtFlowNode(flow.antecedent);
81848200
if (type !== neverType) {
81858201
// If we have an antecedent type (meaning we're reachable in some way), we first
8186-
// attempt to narrow the antecedent type. If that produces the nothing type, then
8202+
// attempt to narrow the antecedent type. If that produces the never type, then
81878203
// we take the type guard as an indication that control could reach here in a
81888204
// manner not understood by the control flow analyzer (e.g. a function argument
81898205
// has an invalid type, or a nested function has possibly made an assignment to a
@@ -8292,10 +8308,10 @@ namespace ts {
82928308

82938309
function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
82948310
if (isMatchingReference(reference, expr)) {
8295-
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
8311+
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy, assumeTrue);
82968312
}
82978313
if (isMatchingPropertyAccess(expr)) {
8298-
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
8314+
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy, assumeTrue));
82998315
}
83008316
return type;
83018317
}
@@ -8353,7 +8369,7 @@ namespace ts {
83538369
value.kind === SyntaxKind.NullKeyword ?
83548370
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
83558371
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
8356-
return getTypeWithFacts(type, facts);
8372+
return getTypeWithFacts(type, facts, assumeTrue);
83578373
}
83588374
if (type.flags & TypeFlags.NotUnionOrUnit) {
83598375
return type;
@@ -8391,7 +8407,7 @@ namespace ts {
83918407
const facts = assumeTrue ?
83928408
getProperty(typeofEQFacts, literal.text) || TypeFacts.TypeofEQHostObject :
83938409
getProperty(typeofNEFacts, literal.text) || TypeFacts.TypeofNEHostObject;
8394-
return getTypeWithFacts(type, facts);
8410+
return getTypeWithFacts(type, facts, assumeTrue);
83958411
}
83968412

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

tests/baselines/reference/controlFlowIfStatement.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ function b() {
3535
}
3636
x; // string
3737
}
38+
function c<T>(data: string | T): T {
39+
if (typeof data === 'string') {
40+
return JSON.parse(data);
41+
}
42+
else {
43+
return data;
44+
}
45+
}
46+
function d<T extends string>(data: string | T): never {
47+
if (typeof data === 'string') {
48+
throw new Error('will always happen');
49+
}
50+
else {
51+
return data;
52+
}
53+
}
3854

3955

4056
//// [controlFlowIfStatement.js]
@@ -72,3 +88,19 @@ function b() {
7288
}
7389
x; // string
7490
}
91+
function c(data) {
92+
if (typeof data === 'string') {
93+
return JSON.parse(data);
94+
}
95+
else {
96+
return data;
97+
}
98+
}
99+
function d(data) {
100+
if (typeof data === 'string') {
101+
throw new Error('will always happen');
102+
}
103+
else {
104+
return data;
105+
}
106+
}

tests/baselines/reference/controlFlowIfStatement.symbols

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,42 @@ function b() {
7171
x; // string
7272
>x : Symbol(x, Decl(controlFlowIfStatement.ts, 26, 7))
7373
}
74+
function c<T>(data: string | T): T {
75+
>c : Symbol(c, Decl(controlFlowIfStatement.ts, 35, 1))
76+
>T : Symbol(T, Decl(controlFlowIfStatement.ts, 36, 11))
77+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 36, 14))
78+
>T : Symbol(T, Decl(controlFlowIfStatement.ts, 36, 11))
79+
>T : Symbol(T, Decl(controlFlowIfStatement.ts, 36, 11))
80+
81+
if (typeof data === 'string') {
82+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 36, 14))
83+
84+
return JSON.parse(data);
85+
>JSON.parse : Symbol(JSON.parse, Decl(lib.d.ts, --, --))
86+
>JSON : Symbol(JSON, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
87+
>parse : Symbol(JSON.parse, Decl(lib.d.ts, --, --))
88+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 36, 14))
89+
}
90+
else {
91+
return data;
92+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 36, 14))
93+
}
94+
}
95+
function d<T extends string>(data: string | T): never {
96+
>d : Symbol(d, Decl(controlFlowIfStatement.ts, 43, 1))
97+
>T : Symbol(T, Decl(controlFlowIfStatement.ts, 44, 11))
98+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 44, 29))
99+
>T : Symbol(T, Decl(controlFlowIfStatement.ts, 44, 11))
100+
101+
if (typeof data === 'string') {
102+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 44, 29))
103+
104+
throw new Error('will always happen');
105+
>Error : Symbol(Error, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
106+
}
107+
else {
108+
return data;
109+
>data : Symbol(data, Decl(controlFlowIfStatement.ts, 44, 29))
110+
}
111+
}
74112

tests/baselines/reference/controlFlowIfStatement.types

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,51 @@ function b() {
9090
x; // string
9191
>x : string
9292
}
93+
function c<T>(data: string | T): T {
94+
>c : <T>(data: string | T) => T
95+
>T : T
96+
>data : string | T
97+
>T : T
98+
>T : T
99+
100+
if (typeof data === 'string') {
101+
>typeof data === 'string' : boolean
102+
>typeof data : string
103+
>data : string | T
104+
>'string' : "string"
105+
106+
return JSON.parse(data);
107+
>JSON.parse(data) : any
108+
>JSON.parse : (text: string, reviver?: (key: any, value: any) => any) => any
109+
>JSON : JSON
110+
>parse : (text: string, reviver?: (key: any, value: any) => any) => any
111+
>data : string & T & (string | T)
112+
}
113+
else {
114+
return data;
115+
>data : T
116+
}
117+
}
118+
function d<T extends string>(data: string | T): never {
119+
>d : <T extends string>(data: string | T) => never
120+
>T : T
121+
>data : string | T
122+
>T : T
123+
124+
if (typeof data === 'string') {
125+
>typeof data === 'string' : boolean
126+
>typeof data : string
127+
>data : string | T
128+
>'string' : "string"
129+
130+
throw new Error('will always happen');
131+
>new Error('will always happen') : Error
132+
>Error : ErrorConstructor
133+
>'will always happen' : string
134+
}
135+
else {
136+
return data;
137+
>data : never
138+
}
139+
}
93140

tests/cases/conformance/controlFlow/controlFlowIfStatement.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,19 @@ function b() {
3434
}
3535
x; // string
3636
}
37+
function c<T>(data: string | T): T {
38+
if (typeof data === 'string') {
39+
return JSON.parse(data);
40+
}
41+
else {
42+
return data;
43+
}
44+
}
45+
function d<T extends string>(data: string | T): never {
46+
if (typeof data === 'string') {
47+
throw new Error('will always happen');
48+
}
49+
else {
50+
return data;
51+
}
52+
}

0 commit comments

Comments
 (0)