Skip to content

Commit 14f0aa0

Browse files
authored
Merge pull request #10118 from Microsoft/limitTypeGuardAssertions
Limit "type guards as assertions" behavior
2 parents 0eeb9cb + 12eb57c commit 14f0aa0

7 files changed

+72
-41
lines changed

src/compiler/checker.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ namespace ts {
215215
const flowLoopKeys: string[] = [];
216216
const flowLoopTypes: Type[][] = [];
217217
const visitedFlowNodes: FlowNode[] = [];
218-
const visitedFlowTypes: Type[] = [];
218+
const visitedFlowTypes: FlowType[] = [];
219219
const potentialThisCollisions: Node[] = [];
220220
const awaitedTypeStack: number[] = [];
221221

@@ -8090,21 +8090,33 @@ namespace ts {
80908090
f(type) ? type : neverType;
80918091
}
80928092

8093+
function isIncomplete(flowType: FlowType) {
8094+
return flowType.flags === 0;
8095+
}
8096+
8097+
function getTypeFromFlowType(flowType: FlowType) {
8098+
return flowType.flags === 0 ? (<IncompleteType>flowType).type : <Type>flowType;
8099+
}
8100+
8101+
function createFlowType(type: Type, incomplete: boolean): FlowType {
8102+
return incomplete ? { flags: 0, type } : type;
8103+
}
8104+
80938105
function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean, includeOuterFunctions: boolean) {
80948106
let key: string;
80958107
if (!reference.flowNode || assumeInitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
80968108
return declaredType;
80978109
}
80988110
const initialType = assumeInitialized ? declaredType : includeFalsyTypes(declaredType, TypeFlags.Undefined);
80998111
const visitedFlowStart = visitedFlowCount;
8100-
const result = getTypeAtFlowNode(reference.flowNode);
8112+
const result = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode));
81018113
visitedFlowCount = visitedFlowStart;
81028114
if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull) === neverType) {
81038115
return declaredType;
81048116
}
81058117
return result;
81068118

8107-
function getTypeAtFlowNode(flow: FlowNode): Type {
8119+
function getTypeAtFlowNode(flow: FlowNode): FlowType {
81088120
while (true) {
81098121
if (flow.flags & FlowFlags.Shared) {
81108122
// We cache results of flow type resolution for shared nodes that were previously visited in
@@ -8116,7 +8128,7 @@ namespace ts {
81168128
}
81178129
}
81188130
}
8119-
let type: Type;
8131+
let type: FlowType;
81208132
if (flow.flags & FlowFlags.Assignment) {
81218133
type = getTypeAtFlowAssignment(<FlowAssignment>flow);
81228134
if (!type) {
@@ -8184,41 +8196,44 @@ namespace ts {
81848196
return undefined;
81858197
}
81868198

8187-
function getTypeAtFlowCondition(flow: FlowCondition) {
8188-
let type = getTypeAtFlowNode(flow.antecedent);
8199+
function getTypeAtFlowCondition(flow: FlowCondition): FlowType {
8200+
const flowType = getTypeAtFlowNode(flow.antecedent);
8201+
let type = getTypeFromFlowType(flowType);
81898202
if (type !== neverType) {
81908203
// If we have an antecedent type (meaning we're reachable in some way), we first
8191-
// attempt to narrow the antecedent type. If that produces the nothing type, then
8192-
// we take the type guard as an indication that control could reach here in a
8193-
// manner not understood by the control flow analyzer (e.g. a function argument
8194-
// has an invalid type, or a nested function has possibly made an assignment to a
8195-
// captured variable). We proceed by reverting to the declared type and then
8204+
// attempt to narrow the antecedent type. If that produces the never type, and if
8205+
// the antecedent type is incomplete (i.e. a transient type in a loop), then we
8206+
// take the type guard as an indication that control *could* reach here once we
8207+
// have the complete type. We proceed by reverting to the declared type and then
81968208
// narrow that.
81978209
const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0;
81988210
type = narrowType(type, flow.expression, assumeTrue);
8199-
if (type === neverType) {
8211+
if (type === neverType && isIncomplete(flowType)) {
82008212
type = narrowType(declaredType, flow.expression, assumeTrue);
82018213
}
82028214
}
8203-
return type;
8215+
return createFlowType(type, isIncomplete(flowType));
82048216
}
82058217

8206-
function getTypeAtSwitchClause(flow: FlowSwitchClause) {
8207-
const type = getTypeAtFlowNode(flow.antecedent);
8218+
function getTypeAtSwitchClause(flow: FlowSwitchClause): FlowType {
8219+
const flowType = getTypeAtFlowNode(flow.antecedent);
8220+
let type = getTypeFromFlowType(flowType);
82088221
const expr = flow.switchStatement.expression;
82098222
if (isMatchingReference(reference, expr)) {
8210-
return narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
8223+
type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
82118224
}
8212-
if (isMatchingPropertyAccess(expr)) {
8213-
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
8225+
else if (isMatchingPropertyAccess(expr)) {
8226+
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
82148227
}
8215-
return type;
8228+
return createFlowType(type, isIncomplete(flowType));
82168229
}
82178230

8218-
function getTypeAtFlowBranchLabel(flow: FlowLabel) {
8231+
function getTypeAtFlowBranchLabel(flow: FlowLabel): FlowType {
82198232
const antecedentTypes: Type[] = [];
8233+
let seenIncomplete = false;
82208234
for (const antecedent of flow.antecedents) {
8221-
const type = getTypeAtFlowNode(antecedent);
8235+
const flowType = getTypeAtFlowNode(antecedent);
8236+
const type = getTypeFromFlowType(flowType);
82228237
// If the type at a particular antecedent path is the declared type and the
82238238
// reference is known to always be assigned (i.e. when declared and initial types
82248239
// are the same), there is no reason to process more antecedents since the only
@@ -8229,11 +8244,14 @@ namespace ts {
82298244
if (!contains(antecedentTypes, type)) {
82308245
antecedentTypes.push(type);
82318246
}
8247+
if (isIncomplete(flowType)) {
8248+
seenIncomplete = true;
8249+
}
82328250
}
8233-
return getUnionType(antecedentTypes);
8251+
return createFlowType(getUnionType(antecedentTypes), seenIncomplete);
82348252
}
82358253

8236-
function getTypeAtFlowLoopLabel(flow: FlowLabel) {
8254+
function getTypeAtFlowLoopLabel(flow: FlowLabel): FlowType {
82378255
// If we have previously computed the control flow type for the reference at
82388256
// this flow loop junction, return the cached type.
82398257
const id = getFlowNodeId(flow);
@@ -8245,12 +8263,12 @@ namespace ts {
82458263
return cache[key];
82468264
}
82478265
// If this flow loop junction and reference are already being processed, return
8248-
// the union of the types computed for each branch so far. We should never see
8249-
// an empty array here because the first antecedent of a loop junction is always
8250-
// the non-looping control flow path that leads to the top.
8266+
// the union of the types computed for each branch so far, marked as incomplete.
8267+
// We should never see an empty array here because the first antecedent of a loop
8268+
// junction is always the non-looping control flow path that leads to the top.
82518269
for (let i = flowLoopStart; i < flowLoopCount; i++) {
82528270
if (flowLoopNodes[i] === flow && flowLoopKeys[i] === key) {
8253-
return getUnionType(flowLoopTypes[i]);
8271+
return createFlowType(getUnionType(flowLoopTypes[i]), /*incomplete*/ true);
82548272
}
82558273
}
82568274
// Add the flow loop junction and reference to the in-process stack and analyze
@@ -8261,7 +8279,7 @@ namespace ts {
82618279
flowLoopTypes[flowLoopCount] = antecedentTypes;
82628280
for (const antecedent of flow.antecedents) {
82638281
flowLoopCount++;
8264-
const type = getTypeAtFlowNode(antecedent);
8282+
const type = getTypeFromFlowType(getTypeAtFlowNode(antecedent));
82658283
flowLoopCount--;
82668284
// If we see a value appear in the cache it is a sign that control flow analysis
82678285
// was restarted and completed by checkExpressionCached. We can simply pick up

src/compiler/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,16 @@ namespace ts {
16061606
antecedent: FlowNode;
16071607
}
16081608

1609+
export type FlowType = Type | IncompleteType;
1610+
1611+
// Incomplete types occur during control flow analysis of loops. An IncompleteType
1612+
// is distinguished from a regular type by a flags value of zero. Incomplete type
1613+
// objects are internal to the getFlowTypeOfRefecence function and never escape it.
1614+
export interface IncompleteType {
1615+
flags: TypeFlags; // No flags set
1616+
type: Type; // The type marked incomplete
1617+
}
1618+
16091619
export interface AmdDependency {
16101620
path: string;
16111621
name: string;

tests/baselines/reference/stringLiteralTypesAndTuples01.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ function rawr(dino: RexOrRaptor) {
5555
throw "Unexpected " + dino;
5656
>"Unexpected " + dino : string
5757
>"Unexpected " : string
58-
>dino : "t-rex"
58+
>dino : never
5959
}

tests/baselines/reference/typeGuardTautologicalConsistiency.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ if (typeof stringOrNumber === "number") {
1515
>"number" : "number"
1616

1717
stringOrNumber;
18-
>stringOrNumber : string
18+
>stringOrNumber : never
1919
}
2020
}
2121

@@ -31,6 +31,6 @@ if (typeof stringOrNumber === "number" && typeof stringOrNumber !== "number") {
3131
>"number" : "number"
3232

3333
stringOrNumber;
34-
>stringOrNumber : string
34+
>stringOrNumber : never
3535
}
3636

tests/baselines/reference/typeGuardTypeOfUndefined.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function test2(a: any) {
4747
>"boolean" : "boolean"
4848

4949
a;
50-
>a : boolean
50+
>a : never
5151
}
5252
else {
5353
a;
@@ -129,7 +129,7 @@ function test5(a: boolean | void) {
129129
}
130130
else {
131131
a;
132-
>a : void
132+
>a : never
133133
}
134134
}
135135
else {
@@ -188,7 +188,7 @@ function test7(a: boolean | void) {
188188
}
189189
else {
190190
a;
191-
>a : void
191+
>a : never
192192
}
193193
}
194194

tests/baselines/reference/typeGuardsAsAssertions.types

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,10 @@ function f1() {
193193
>x : undefined
194194

195195
x; // string | number (guard as assertion)
196-
>x : string | number
196+
>x : never
197197
}
198198
x; // string | number | undefined
199-
>x : string | number | undefined
199+
>x : undefined
200200
}
201201

202202
function f2() {
@@ -216,10 +216,10 @@ function f2() {
216216
>"string" : "string"
217217

218218
x; // string (guard as assertion)
219-
>x : string
219+
>x : never
220220
}
221221
x; // string | undefined
222-
>x : string | undefined
222+
>x : undefined
223223
}
224224

225225
function f3() {
@@ -239,7 +239,7 @@ function f3() {
239239
return;
240240
}
241241
x; // string | number (guard as assertion)
242-
>x : string | number
242+
>x : never
243243
}
244244

245245
function f4() {
@@ -281,7 +281,7 @@ function f5(x: string | number) {
281281
>"number" : "number"
282282

283283
x; // number (guard as assertion)
284-
>x : number
284+
>x : never
285285
}
286286
else {
287287
x; // string | number

tests/baselines/reference/typeGuardsInIfStatement.errors.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts(22,10): error TS2354: No best common type exists among return expressions.
22
tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts(31,10): error TS2354: No best common type exists among return expressions.
33
tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts(49,10): error TS2354: No best common type exists among return expressions.
4+
tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts(139,17): error TS2339: Property 'toString' does not exist on type 'never'.
45

56

6-
==== tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts (3 errors) ====
7+
==== tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts (4 errors) ====
78
// In the true branch statement of an 'if' statement,
89
// the type of a variable or parameter is narrowed by any type guard in the 'if' condition when true.
910
// In the false branch statement of an 'if' statement,
@@ -149,5 +150,7 @@ tests/cases/conformance/expressions/typeGuards/typeGuardsInIfStatement.ts(49,10)
149150
return typeof x === "number"
150151
? x.toString() // number
151152
: x.toString(); // boolean | string
153+
~~~~~~~~
154+
!!! error TS2339: Property 'toString' does not exist on type 'never'.
152155
}
153156
}

0 commit comments

Comments
 (0)