Skip to content

Commit 0c24ccc

Browse files
committed
Infer type predicates from function bodies
1 parent 60f93aa commit 0c24ccc

13 files changed

+2402
-83
lines changed

src/compiler/checker.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15458,9 +15458,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1545815458
jsdocPredicate = getTypePredicateOfSignature(jsdocSignature);
1545915459
}
1546015460
}
15461-
signature.resolvedTypePredicate = type && isTypePredicateNode(type) ?
15462-
createTypePredicateFromTypePredicateNode(type, signature) :
15463-
jsdocPredicate || noTypePredicate;
15461+
if (type || jsdocPredicate) {
15462+
signature.resolvedTypePredicate = type && isTypePredicateNode(type) ?
15463+
createTypePredicateFromTypePredicateNode(type, signature) :
15464+
jsdocPredicate || noTypePredicate;
15465+
} else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) {
15466+
const {declaration} = signature;
15467+
signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop
15468+
signature.resolvedTypePredicate = getTypePredicateFromBody(declaration, signature) || noTypePredicate;
15469+
} else {
15470+
signature.resolvedTypePredicate = noTypePredicate;
15471+
}
1546415472
}
1546515473
Debug.assert(!!signature.resolvedTypePredicate);
1546615474
}
@@ -37389,6 +37397,80 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3738937397
}
3739037398
}
3739137399

37400+
function getTypePredicateFromBody(func: FunctionLikeDeclaration, _sig: Signature): TypePredicate | undefined {
37401+
const functionFlags = getFunctionFlags(func);
37402+
if (functionFlags !== FunctionFlags.Normal) return undefined;
37403+
37404+
// Only attempt to infer a type predicate if there's exactly one return.
37405+
let singleReturn: Expression | undefined;
37406+
if (func.body && func.body.kind !== SyntaxKind.Block) {
37407+
singleReturn = func.body; // arrow function
37408+
} else {
37409+
if (functionHasImplicitReturn(func)) return undefined;
37410+
37411+
const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => {
37412+
if (singleReturn || !returnStatement.expression) return true;
37413+
singleReturn = returnStatement.expression;
37414+
});
37415+
if (bailedEarly || !singleReturn) return undefined;
37416+
}
37417+
37418+
const predicate = checkIfExpressionRefinesAnyParameter(singleReturn);
37419+
if (predicate) {
37420+
const [i, type] = predicate;
37421+
const param = func.parameters[i];
37422+
if (isIdentifier(param.name)) {
37423+
// TODO: is there an alternative to the "as string" here? (It's __String)
37424+
return createTypePredicate(TypePredicateKind.Identifier, param.name.escapedText as string, i, type);
37425+
}
37426+
}
37427+
return undefined;
37428+
37429+
function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined {
37430+
expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true);
37431+
const type = checkExpressionCached(expr, CheckMode.TypeOnly);
37432+
if (type !== booleanType || !func.body) return undefined;
37433+
37434+
return forEach(func.parameters, (param, i) => {
37435+
const initType = getSymbolLinks(param.symbol).type;
37436+
if (!initType || initType === booleanType || isSymbolAssigned(param.symbol)) {
37437+
// Refining "x: boolean" to "x is true" or "x is false" isn't useful.
37438+
return;
37439+
}
37440+
const trueType = checkIfExpressionRefinesParameter(expr, param, initType);
37441+
if (trueType) {
37442+
return [i, trueType];
37443+
}
37444+
});
37445+
}
37446+
37447+
function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined {
37448+
const antecedent = (expr as Expression & {flowNode?: FlowNode}).flowNode ?? { flags: FlowFlags.Start };
37449+
const trueCondition: FlowCondition = {
37450+
flags: FlowFlags.TrueCondition,
37451+
node: expr,
37452+
antecedent,
37453+
};
37454+
37455+
const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition);
37456+
if (trueType === initType) return undefined;
37457+
37458+
// "x is T" means that x is T if and only if it returns true. If it returns false then x is not T.
37459+
// However, TS may not be able to represent "not T", in which case we can be more lax.
37460+
// It's safe to infer a type guard if falseType = Exclude<initType, trueType>
37461+
// This matches what you'd get if you called the type guard in an if/else statement.
37462+
const falseCondition: FlowCondition = {
37463+
...trueCondition,
37464+
flags: FlowFlags.FalseCondition,
37465+
}
37466+
const falseType = getFlowTypeOfReference(param.name, initType, initType, func, falseCondition);
37467+
const candidateFalse = filterType(initType, t => !isTypeSubtypeOf(t, trueType));
37468+
if (isTypeIdenticalTo(candidateFalse, falseType)) {
37469+
return trueType;
37470+
}
37471+
}
37472+
}
37473+
3739237474
/**
3739337475
* TypeScript Specification 1.0 (6.3) - July 2014
3739437476
* An explicitly typed function whose return type isn't the Void type,

tests/baselines/reference/findLast(target=esnext).types

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,133 +3,133 @@
33
=== findLast.ts ===
44
const itemNumber: number | undefined = [0].findLast((item) => item === 0);
55
>itemNumber : number
6-
>[0].findLast((item) => item === 0) : number
6+
>[0].findLast((item) => item === 0) : 0
77
>[0].findLast : { <S extends number>(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number; }
88
>[0] : number[]
99
>0 : 0
1010
>findLast : { <S extends number>(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number; }
11-
>(item) => item === 0 : (item: number) => boolean
11+
>(item) => item === 0 : (item: number) => item is 0
1212
>item : number
1313
>item === 0 : boolean
1414
>item : number
1515
>0 : 0
1616

1717
const itemString: string | undefined = ["string"].findLast((item) => item === "string");
1818
>itemString : string
19-
>["string"].findLast((item) => item === "string") : string
19+
>["string"].findLast((item) => item === "string") : "string"
2020
>["string"].findLast : { <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S; (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string; }
2121
>["string"] : string[]
2222
>"string" : "string"
2323
>findLast : { <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S; (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string; }
24-
>(item) => item === "string" : (item: string) => boolean
24+
>(item) => item === "string" : (item: string) => item is "string"
2525
>item : string
2626
>item === "string" : boolean
2727
>item : string
2828
>"string" : "string"
2929

3030
new Int8Array().findLast((item) => item === 0);
31-
>new Int8Array().findLast((item) => item === 0) : number
31+
>new Int8Array().findLast((item) => item === 0) : 0
3232
>new Int8Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Int8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): number; }
3333
>new Int8Array() : Int8Array
3434
>Int8Array : Int8ArrayConstructor
3535
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Int8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): number; }
36-
>(item) => item === 0 : (item: number) => boolean
36+
>(item) => item === 0 : (item: number) => item is 0
3737
>item : number
3838
>item === 0 : boolean
3939
>item : number
4040
>0 : 0
4141

4242
new Uint8Array().findLast((item) => item === 0);
43-
>new Uint8Array().findLast((item) => item === 0) : number
43+
>new Uint8Array().findLast((item) => item === 0) : 0
4444
>new Uint8Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): number; }
4545
>new Uint8Array() : Uint8Array
4646
>Uint8Array : Uint8ArrayConstructor
4747
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): number; }
48-
>(item) => item === 0 : (item: number) => boolean
48+
>(item) => item === 0 : (item: number) => item is 0
4949
>item : number
5050
>item === 0 : boolean
5151
>item : number
5252
>0 : 0
5353

5454
new Uint8ClampedArray().findLast((item) => item === 0);
55-
>new Uint8ClampedArray().findLast((item) => item === 0) : number
55+
>new Uint8ClampedArray().findLast((item) => item === 0) : 0
5656
>new Uint8ClampedArray().findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint8ClampedArray) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): number; }
5757
>new Uint8ClampedArray() : Uint8ClampedArray
5858
>Uint8ClampedArray : Uint8ClampedArrayConstructor
5959
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint8ClampedArray) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): number; }
60-
>(item) => item === 0 : (item: number) => boolean
60+
>(item) => item === 0 : (item: number) => item is 0
6161
>item : number
6262
>item === 0 : boolean
6363
>item : number
6464
>0 : 0
6565

6666
new Int16Array().findLast((item) => item === 0);
67-
>new Int16Array().findLast((item) => item === 0) : number
67+
>new Int16Array().findLast((item) => item === 0) : 0
6868
>new Int16Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Int16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): number; }
6969
>new Int16Array() : Int16Array
7070
>Int16Array : Int16ArrayConstructor
7171
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Int16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): number; }
72-
>(item) => item === 0 : (item: number) => boolean
72+
>(item) => item === 0 : (item: number) => item is 0
7373
>item : number
7474
>item === 0 : boolean
7575
>item : number
7676
>0 : 0
7777

7878
new Uint16Array().findLast((item) => item === 0);
79-
>new Uint16Array().findLast((item) => item === 0) : number
79+
>new Uint16Array().findLast((item) => item === 0) : 0
8080
>new Uint16Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): number; }
8181
>new Uint16Array() : Uint16Array
8282
>Uint16Array : Uint16ArrayConstructor
8383
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): number; }
84-
>(item) => item === 0 : (item: number) => boolean
84+
>(item) => item === 0 : (item: number) => item is 0
8585
>item : number
8686
>item === 0 : boolean
8787
>item : number
8888
>0 : 0
8989

9090
new Int32Array().findLast((item) => item === 0);
91-
>new Int32Array().findLast((item) => item === 0) : number
91+
>new Int32Array().findLast((item) => item === 0) : 0
9292
>new Int32Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Int32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): number; }
9393
>new Int32Array() : Int32Array
9494
>Int32Array : Int32ArrayConstructor
9595
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Int32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): number; }
96-
>(item) => item === 0 : (item: number) => boolean
96+
>(item) => item === 0 : (item: number) => item is 0
9797
>item : number
9898
>item === 0 : boolean
9999
>item : number
100100
>0 : 0
101101

102102
new Uint32Array().findLast((item) => item === 0);
103-
>new Uint32Array().findLast((item) => item === 0) : number
103+
>new Uint32Array().findLast((item) => item === 0) : 0
104104
>new Uint32Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): number; }
105105
>new Uint32Array() : Uint32Array
106106
>Uint32Array : Uint32ArrayConstructor
107107
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Uint32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): number; }
108-
>(item) => item === 0 : (item: number) => boolean
108+
>(item) => item === 0 : (item: number) => item is 0
109109
>item : number
110110
>item === 0 : boolean
111111
>item : number
112112
>0 : 0
113113

114114
new Float32Array().findLast((item) => item === 0);
115-
>new Float32Array().findLast((item) => item === 0) : number
115+
>new Float32Array().findLast((item) => item === 0) : 0
116116
>new Float32Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Float32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): number; }
117117
>new Float32Array() : Float32Array
118118
>Float32Array : Float32ArrayConstructor
119119
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Float32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): number; }
120-
>(item) => item === 0 : (item: number) => boolean
120+
>(item) => item === 0 : (item: number) => item is 0
121121
>item : number
122122
>item === 0 : boolean
123123
>item : number
124124
>0 : 0
125125

126126
new Float64Array().findLast((item) => item === 0);
127-
>new Float64Array().findLast((item) => item === 0) : number
127+
>new Float64Array().findLast((item) => item === 0) : 0
128128
>new Float64Array().findLast : { <S extends number>(predicate: (value: number, index: number, array: Float64Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): number; }
129129
>new Float64Array() : Float64Array
130130
>Float64Array : Float64ArrayConstructor
131131
>findLast : { <S extends number>(predicate: (value: number, index: number, array: Float64Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): number; }
132-
>(item) => item === 0 : (item: number) => boolean
132+
>(item) => item === 0 : (item: number) => item is 0
133133
>item : number
134134
>item === 0 : boolean
135135
>item : number
@@ -170,7 +170,7 @@ const indexNumber: number = [0].findLastIndex((item) => item === 0);
170170
>[0] : number[]
171171
>0 : 0
172172
>findLastIndex : (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any) => number
173-
>(item) => item === 0 : (item: number) => boolean
173+
>(item) => item === 0 : (item: number) => item is 0
174174
>item : number
175175
>item === 0 : boolean
176176
>item : number
@@ -183,7 +183,7 @@ const indexString: number = ["string"].findLastIndex((item) => item === "string"
183183
>["string"] : string[]
184184
>"string" : "string"
185185
>findLastIndex : (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any) => number
186-
>(item) => item === "string" : (item: string) => boolean
186+
>(item) => item === "string" : (item: string) => item is "string"
187187
>item : string
188188
>item === "string" : boolean
189189
>item : string
@@ -195,7 +195,7 @@ new Int8Array().findLastIndex((item) => item === 0);
195195
>new Int8Array() : Int8Array
196196
>Int8Array : Int8ArrayConstructor
197197
>findLastIndex : (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any) => number
198-
>(item) => item === 0 : (item: number) => boolean
198+
>(item) => item === 0 : (item: number) => item is 0
199199
>item : number
200200
>item === 0 : boolean
201201
>item : number
@@ -207,7 +207,7 @@ new Uint8Array().findLastIndex((item) => item === 0);
207207
>new Uint8Array() : Uint8Array
208208
>Uint8Array : Uint8ArrayConstructor
209209
>findLastIndex : (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any) => number
210-
>(item) => item === 0 : (item: number) => boolean
210+
>(item) => item === 0 : (item: number) => item is 0
211211
>item : number
212212
>item === 0 : boolean
213213
>item : number
@@ -219,7 +219,7 @@ new Uint8ClampedArray().findLastIndex((item) => item === 0);
219219
>new Uint8ClampedArray() : Uint8ClampedArray
220220
>Uint8ClampedArray : Uint8ClampedArrayConstructor
221221
>findLastIndex : (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any) => number
222-
>(item) => item === 0 : (item: number) => boolean
222+
>(item) => item === 0 : (item: number) => item is 0
223223
>item : number
224224
>item === 0 : boolean
225225
>item : number
@@ -231,7 +231,7 @@ new Int16Array().findLastIndex((item) => item === 0);
231231
>new Int16Array() : Int16Array
232232
>Int16Array : Int16ArrayConstructor
233233
>findLastIndex : (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any) => number
234-
>(item) => item === 0 : (item: number) => boolean
234+
>(item) => item === 0 : (item: number) => item is 0
235235
>item : number
236236
>item === 0 : boolean
237237
>item : number
@@ -243,7 +243,7 @@ new Uint16Array().findLastIndex((item) => item === 0);
243243
>new Uint16Array() : Uint16Array
244244
>Uint16Array : Uint16ArrayConstructor
245245
>findLastIndex : (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any) => number
246-
>(item) => item === 0 : (item: number) => boolean
246+
>(item) => item === 0 : (item: number) => item is 0
247247
>item : number
248248
>item === 0 : boolean
249249
>item : number
@@ -255,7 +255,7 @@ new Int32Array().findLastIndex((item) => item === 0);
255255
>new Int32Array() : Int32Array
256256
>Int32Array : Int32ArrayConstructor
257257
>findLastIndex : (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any) => number
258-
>(item) => item === 0 : (item: number) => boolean
258+
>(item) => item === 0 : (item: number) => item is 0
259259
>item : number
260260
>item === 0 : boolean
261261
>item : number
@@ -267,7 +267,7 @@ new Uint32Array().findLastIndex((item) => item === 0);
267267
>new Uint32Array() : Uint32Array
268268
>Uint32Array : Uint32ArrayConstructor
269269
>findLastIndex : (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any) => number
270-
>(item) => item === 0 : (item: number) => boolean
270+
>(item) => item === 0 : (item: number) => item is 0
271271
>item : number
272272
>item === 0 : boolean
273273
>item : number
@@ -279,7 +279,7 @@ new Float32Array().findLastIndex((item) => item === 0);
279279
>new Float32Array() : Float32Array
280280
>Float32Array : Float32ArrayConstructor
281281
>findLastIndex : (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any) => number
282-
>(item) => item === 0 : (item: number) => boolean
282+
>(item) => item === 0 : (item: number) => item is 0
283283
>item : number
284284
>item === 0 : boolean
285285
>item : number
@@ -291,7 +291,7 @@ new Float64Array().findLastIndex((item) => item === 0);
291291
>new Float64Array() : Float64Array
292292
>Float64Array : Float64ArrayConstructor
293293
>findLastIndex : (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any) => number
294-
>(item) => item === 0 : (item: number) => boolean
294+
>(item) => item === 0 : (item: number) => item is 0
295295
>item : number
296296
>item === 0 : boolean
297297
>item : number

0 commit comments

Comments
 (0)