Skip to content

Commit e5bf594

Browse files
danvkahejlsberg
andauthored
Infer type predicates from function bodies using control flow analysis (#57465)
Co-authored-by: Anders Hejlsberg <andersh@microsoft.com>
1 parent 60cf791 commit e5bf594

18 files changed

+3229
-97
lines changed

src/compiler/binder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
10941094
inAssignmentPattern = saveInAssignmentPattern;
10951095
return;
10961096
}
1097-
if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && !options.allowUnreachableCode) {
1097+
if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && (!options.allowUnreachableCode || node.kind === SyntaxKind.ReturnStatement)) {
10981098
(node as HasFlowNode).flowNode = currentFlow;
10991099
}
11001100
switch (node.kind) {

src/compiler/checker.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6456,6 +6456,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
64566456
function createNodeBuilder() {
64576457
return {
64586458
typeToTypeNode: (type: Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeToTypeNodeHelper(type, context)),
6459+
typePredicateToTypePredicateNode: (typePredicate: TypePredicate, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typePredicateToTypePredicateNodeHelper(typePredicate, context)),
64596460
indexInfoToIndexSignatureDeclaration: (indexInfo: IndexInfo, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, /*typeNode*/ undefined)),
64606461
signatureToSignatureDeclaration: (signature: Signature, kind: SignatureDeclaration["kind"], enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => signatureToSignatureDeclarationHelper(signature, kind, context)),
64616462
symbolToEntityName: (symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToName(symbol, context, meaning, /*expectsIdentifier*/ false)),
@@ -7704,14 +7705,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
77047705
let returnTypeNode: TypeNode | undefined;
77057706
const typePredicate = getTypePredicateOfSignature(signature);
77067707
if (typePredicate) {
7707-
const assertsModifier = typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ?
7708-
factory.createToken(SyntaxKind.AssertsKeyword) :
7709-
undefined;
7710-
const parameterName = typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ?
7711-
setEmitFlags(factory.createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) :
7712-
factory.createThisTypeNode();
7713-
const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context);
7714-
returnTypeNode = factory.createTypePredicateNode(assertsModifier, parameterName, typeNode);
7708+
returnTypeNode = typePredicateToTypePredicateNodeHelper(typePredicate, context);
77157709
}
77167710
else {
77177711
const returnType = getReturnTypeOfSignature(signature);
@@ -7790,6 +7784,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
77907784
return typeParameterToDeclarationWithConstraint(type, context, constraintNode);
77917785
}
77927786

7787+
function typePredicateToTypePredicateNodeHelper(typePredicate: TypePredicate, context: NodeBuilderContext): TypePredicateNode {
7788+
const assertsModifier = typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ?
7789+
factory.createToken(SyntaxKind.AssertsKeyword) :
7790+
undefined;
7791+
const parameterName = typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ?
7792+
setEmitFlags(factory.createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) :
7793+
factory.createThisTypeNode();
7794+
const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context);
7795+
return factory.createTypePredicateNode(assertsModifier, parameterName, typeNode);
7796+
}
7797+
77937798
function getEffectiveParameterDeclaration(parameterSymbol: Symbol): ParameterDeclaration | JSDocParameterTag | undefined {
77947799
const parameterDeclaration: ParameterDeclaration | JSDocParameterTag | undefined = getDeclarationOfKind<ParameterDeclaration>(parameterSymbol, SyntaxKind.Parameter);
77957800
if (parameterDeclaration) {
@@ -10309,11 +10314,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1030910314
return writer ? typePredicateToStringWorker(writer).getText() : usingSingleLineStringWriter(typePredicateToStringWorker);
1031010315

1031110316
function typePredicateToStringWorker(writer: EmitTextWriter) {
10312-
const predicate = factory.createTypePredicateNode(
10313-
typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createToken(SyntaxKind.AssertsKeyword) : undefined,
10314-
typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createIdentifier(typePredicate.parameterName) : factory.createThisTypeNode(),
10315-
typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)!, // TODO: GH#18217
10316-
);
10317+
const nodeBuilderFlags = toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName;
10318+
const predicate = nodeBuilder.typePredicateToTypePredicateNode(typePredicate, enclosingDeclaration, nodeBuilderFlags)!; // TODO: GH#18217
1031710319
const printer = createPrinterWithRemoveComments();
1031810320
const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration);
1031910321
printer.writeNode(EmitHint.Unspecified, predicate, /*sourceFile*/ sourceFile, writer);
@@ -15476,9 +15478,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1547615478
jsdocPredicate = getTypePredicateOfSignature(jsdocSignature);
1547715479
}
1547815480
}
15479-
signature.resolvedTypePredicate = type && isTypePredicateNode(type) ?
15480-
createTypePredicateFromTypePredicateNode(type, signature) :
15481-
jsdocPredicate || noTypePredicate;
15481+
if (type || jsdocPredicate) {
15482+
signature.resolvedTypePredicate = type && isTypePredicateNode(type) ?
15483+
createTypePredicateFromTypePredicateNode(type, signature) :
15484+
jsdocPredicate || noTypePredicate;
15485+
}
15486+
else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType.flags & TypeFlags.Boolean) && getParameterCount(signature) > 0) {
15487+
const { declaration } = signature;
15488+
signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop
15489+
signature.resolvedTypePredicate = getTypePredicateFromBody(declaration) || noTypePredicate;
15490+
}
15491+
else {
15492+
signature.resolvedTypePredicate = noTypePredicate;
15493+
}
1548215494
}
1548315495
Debug.assert(!!signature.resolvedTypePredicate);
1548415496
}
@@ -37450,6 +37462,72 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3745037462
}
3745137463
}
3745237464

37465+
function getTypePredicateFromBody(func: FunctionLikeDeclaration): TypePredicate | undefined {
37466+
switch (func.kind) {
37467+
case SyntaxKind.Constructor:
37468+
case SyntaxKind.GetAccessor:
37469+
case SyntaxKind.SetAccessor:
37470+
return undefined;
37471+
}
37472+
const functionFlags = getFunctionFlags(func);
37473+
if (functionFlags !== FunctionFlags.Normal) return undefined;
37474+
37475+
// Only attempt to infer a type predicate if there's exactly one return.
37476+
let singleReturn: Expression | undefined;
37477+
if (func.body && func.body.kind !== SyntaxKind.Block) {
37478+
singleReturn = func.body; // arrow function
37479+
}
37480+
else {
37481+
const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => {
37482+
if (singleReturn || !returnStatement.expression) return true;
37483+
singleReturn = returnStatement.expression;
37484+
});
37485+
if (bailedEarly || !singleReturn || functionHasImplicitReturn(func)) return undefined;
37486+
}
37487+
return checkIfExpressionRefinesAnyParameter(func, singleReturn);
37488+
}
37489+
37490+
function checkIfExpressionRefinesAnyParameter(func: FunctionLikeDeclaration, expr: Expression): TypePredicate | undefined {
37491+
expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true);
37492+
const returnType = checkExpressionCached(expr);
37493+
if (!(returnType.flags & TypeFlags.Boolean)) return undefined;
37494+
37495+
return forEach(func.parameters, (param, i) => {
37496+
const initType = getTypeOfSymbol(param.symbol);
37497+
if (!initType || initType.flags & TypeFlags.Boolean || !isIdentifier(param.name) || isSymbolAssigned(param.symbol) || isRestParameter(param)) {
37498+
// Refining "x: boolean" to "x is true" or "x is false" isn't useful.
37499+
return;
37500+
}
37501+
const trueType = checkIfExpressionRefinesParameter(func, expr, param, initType);
37502+
if (trueType) {
37503+
return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, trueType);
37504+
}
37505+
});
37506+
}
37507+
37508+
function checkIfExpressionRefinesParameter(func: FunctionLikeDeclaration, expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined {
37509+
const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode ||
37510+
expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode ||
37511+
{ flags: FlowFlags.Start };
37512+
const trueCondition: FlowCondition = {
37513+
flags: FlowFlags.TrueCondition,
37514+
node: expr,
37515+
antecedent,
37516+
};
37517+
37518+
const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition);
37519+
if (trueType === initType) return undefined;
37520+
37521+
// "x is T" means that x is T if and only if it returns true. If it returns false then x is not T.
37522+
// This means that if the function is called with an argument of type trueType, there can't be anything left in the `else` branch. It must reduce to `never`.
37523+
const falseCondition: FlowCondition = {
37524+
...trueCondition,
37525+
flags: FlowFlags.FalseCondition,
37526+
};
37527+
const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition);
37528+
return falseSubtype.flags & TypeFlags.Never ? trueType : undefined;
37529+
}
37530+
3745337531
/**
3745437532
* TypeScript Specification 1.0 (6.3) - July 2014
3745537533
* An explicitly typed function whose return type isn't the Void type,
@@ -48511,6 +48589,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4851148589
return factory.createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode;
4851248590
}
4851348591
const signature = getSignatureFromDeclaration(signatureDeclaration);
48592+
const typePredicate = getTypePredicateOfSignature(signature);
48593+
if (typePredicate) {
48594+
// Inferred type predicates
48595+
return nodeBuilder.typePredicateToTypePredicateNode(typePredicate, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker);
48596+
}
4851448597
return nodeBuilder.typeToTypeNode(getReturnTypeOfSignature(signature), enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker);
4851548598
}
4851648599

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//// [tests/cases/compiler/circularConstructorWithReturn.ts] ////
2+
3+
//// [circularConstructorWithReturn.ts]
4+
// This should not be a circularity error. See
5+
// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216
6+
export type Client = ReturnType<typeof getPrismaClient> extends new () => infer T ? T : never
7+
8+
export function getPrismaClient(options?: any) {
9+
class PrismaClient {
10+
self: Client;
11+
constructor(options?: any) {
12+
return (this.self = applyModelsAndClientExtensions(this));
13+
}
14+
}
15+
16+
return PrismaClient
17+
}
18+
19+
export function applyModelsAndClientExtensions(client: Client) {
20+
return client;
21+
}
22+
23+
24+
//// [circularConstructorWithReturn.js]
25+
"use strict";
26+
Object.defineProperty(exports, "__esModule", { value: true });
27+
exports.getPrismaClient = getPrismaClient;
28+
exports.applyModelsAndClientExtensions = applyModelsAndClientExtensions;
29+
function getPrismaClient(options) {
30+
var PrismaClient = /** @class */ (function () {
31+
function PrismaClient(options) {
32+
return (this.self = applyModelsAndClientExtensions(this));
33+
}
34+
return PrismaClient;
35+
}());
36+
return PrismaClient;
37+
}
38+
function applyModelsAndClientExtensions(client) {
39+
return client;
40+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//// [tests/cases/compiler/circularConstructorWithReturn.ts] ////
2+
3+
=== circularConstructorWithReturn.ts ===
4+
// This should not be a circularity error. See
5+
// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216
6+
export type Client = ReturnType<typeof getPrismaClient> extends new () => infer T ? T : never
7+
>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0))
8+
>ReturnType : Symbol(ReturnType, Decl(lib.es5.d.ts, --, --))
9+
>getPrismaClient : Symbol(getPrismaClient, Decl(circularConstructorWithReturn.ts, 2, 93))
10+
>T : Symbol(T, Decl(circularConstructorWithReturn.ts, 2, 79))
11+
>T : Symbol(T, Decl(circularConstructorWithReturn.ts, 2, 79))
12+
13+
export function getPrismaClient(options?: any) {
14+
>getPrismaClient : Symbol(getPrismaClient, Decl(circularConstructorWithReturn.ts, 2, 93))
15+
>options : Symbol(options, Decl(circularConstructorWithReturn.ts, 4, 32))
16+
17+
class PrismaClient {
18+
>PrismaClient : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48))
19+
20+
self: Client;
21+
>self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22))
22+
>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0))
23+
24+
constructor(options?: any) {
25+
>options : Symbol(options, Decl(circularConstructorWithReturn.ts, 7, 16))
26+
27+
return (this.self = applyModelsAndClientExtensions(this));
28+
>this.self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22))
29+
>this : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48))
30+
>self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22))
31+
>applyModelsAndClientExtensions : Symbol(applyModelsAndClientExtensions, Decl(circularConstructorWithReturn.ts, 13, 1))
32+
>this : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48))
33+
}
34+
}
35+
36+
return PrismaClient
37+
>PrismaClient : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48))
38+
}
39+
40+
export function applyModelsAndClientExtensions(client: Client) {
41+
>applyModelsAndClientExtensions : Symbol(applyModelsAndClientExtensions, Decl(circularConstructorWithReturn.ts, 13, 1))
42+
>client : Symbol(client, Decl(circularConstructorWithReturn.ts, 15, 47))
43+
>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0))
44+
45+
return client;
46+
>client : Symbol(client, Decl(circularConstructorWithReturn.ts, 15, 47))
47+
}
48+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//// [tests/cases/compiler/circularConstructorWithReturn.ts] ////
2+
3+
=== circularConstructorWithReturn.ts ===
4+
// This should not be a circularity error. See
5+
// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216
6+
export type Client = ReturnType<typeof getPrismaClient> extends new () => infer T ? T : never
7+
>Client : PrismaClient
8+
>getPrismaClient : (options?: any) => typeof PrismaClient
9+
10+
export function getPrismaClient(options?: any) {
11+
>getPrismaClient : (options?: any) => typeof PrismaClient
12+
>options : any
13+
14+
class PrismaClient {
15+
>PrismaClient : PrismaClient
16+
17+
self: Client;
18+
>self : PrismaClient
19+
20+
constructor(options?: any) {
21+
>options : any
22+
23+
return (this.self = applyModelsAndClientExtensions(this));
24+
>(this.self = applyModelsAndClientExtensions(this)) : PrismaClient
25+
>this.self = applyModelsAndClientExtensions(this) : PrismaClient
26+
>this.self : PrismaClient
27+
>this : this
28+
>self : PrismaClient
29+
>applyModelsAndClientExtensions(this) : PrismaClient
30+
>applyModelsAndClientExtensions : (client: PrismaClient) => PrismaClient
31+
>this : this
32+
}
33+
}
34+
35+
return PrismaClient
36+
>PrismaClient : typeof PrismaClient
37+
}
38+
39+
export function applyModelsAndClientExtensions(client: Client) {
40+
>applyModelsAndClientExtensions : (client: Client) => PrismaClient
41+
>client : PrismaClient
42+
43+
return client;
44+
>client : PrismaClient
45+
}
46+

0 commit comments

Comments
 (0)