From a4ff6b4d87a5b18a340b24d5454b119c7af48856 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 29 Feb 2024 16:19:27 -0800 Subject: [PATCH 1/2] Always bind flow nodes to return statements + other fixes --- src/compiler/binder.ts | 2 +- src/compiler/checker.ts | 31 +++++++++---------------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 7e9f3ced07f..081bb1fb5e9 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1094,7 +1094,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void { inAssignmentPattern = saveInAssignmentPattern; return; } - if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && !options.allowUnreachableCode) { + if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && (!options.allowUnreachableCode || node.kind === SyntaxKind.ReturnStatement)) { (node as HasFlowNode).flowNode = currentFlow; } switch (node.kind) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 307b0850392..095ba1a7b94 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15463,7 +15463,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { createTypePredicateFromTypePredicateNode(type, signature) : jsdocPredicate || noTypePredicate; } - else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) { + else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType.flags & TypeFlags.Boolean)) { const { declaration } = signature; signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop signature.resolvedTypePredicate = getTypePredicateFromBody(declaration) || noTypePredicate; @@ -37411,7 +37411,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Only attempt to infer a type predicate if there's exactly one return. let singleReturn: Expression | undefined; - let singleReturnStatement: ReturnStatement | undefined; if (func.body && func.body.kind !== SyntaxKind.Block) { singleReturn = func.body; // arrow function } @@ -37420,7 +37419,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => { if (singleReturn || !returnStatement.expression) return true; - singleReturnStatement = returnStatement; singleReturn = returnStatement.expression; }); if (bailedEarly || !singleReturn) return undefined; @@ -37431,8 +37429,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const [i, type] = predicate; const param = func.parameters[i]; if (isIdentifier(param.name)) { - // TODO: is there an alternative to the "as string" here? (It's __String) - return createTypePredicate(TypePredicateKind.Identifier, param.name.escapedText as string, i, type); + return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, type); } } return undefined; @@ -37440,11 +37437,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined { expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); const type = checkExpressionCached(expr); - if (type !== booleanType) return undefined; + if (!(type.flags & TypeFlags.Boolean)) return undefined; return forEach(func.parameters, (param, i) => { - const initType = getSymbolLinks(param.symbol).type; - if (!initType || initType === booleanType || isSymbolAssigned(param.symbol)) { + const initType = getTypeOfSymbol(param.symbol); + if (!initType || initType.flags & TypeFlags.Boolean || isSymbolAssigned(param.symbol)) { // Refining "x: boolean" to "x is true" or "x is false" isn't useful. return; } @@ -37456,7 +37453,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { - const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode ?? { flags: FlowFlags.Start }; + const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || + expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode || + { flags: FlowFlags.Start }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, node: expr, @@ -37473,19 +37472,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { flags: FlowFlags.FalseCondition, }; const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); - if (!isTypeIdenticalTo(falseSubtype, neverType)) return undefined; - - // the parameter type may already have been narrowed due to an assertion. - // There's no precise way to represent an assertion that's also a predicate. Best not to try. - // We do this check last since it's unlikely to filter out many possible predicates. - if (singleReturnStatement?.flowNode) { - const typeAtReturn = getFlowTypeOfReference(param.name, initType, initType, func, singleReturnStatement?.flowNode); - if (typeAtReturn !== initType) { - return undefined; - } - } - - return trueType; + return falseSubtype.flags & TypeFlags.Never ? trueType : undefined; } } From 869422f2a543f2028b1ee05b86ab27f5a17cd765 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 29 Feb 2024 16:19:44 -0800 Subject: [PATCH 2/2] Accept new baselines --- tests/baselines/reference/inferTypePredicates.errors.txt | 7 +------ tests/baselines/reference/inferTypePredicates.types | 8 ++++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index e9cf945b5bd..2789e3c3b87 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -14,11 +14,9 @@ inferTypePredicates.ts(113,7): error TS2322: Type 'string | number' is not assig inferTypePredicates.ts(115,7): error TS2322: Type 'string | number' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'. inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1' but required in type 'C2'. -inferTypePredicates.ts(252,7): error TS2322: Type 'string | number | Date' is not assignable to type 'string'. - Type 'number' is not assignable to type 'string'. -==== inferTypePredicates.ts (11 errors) ==== +==== inferTypePredicates.ts (10 errors) ==== // https://github.com/microsoft/TypeScript/issues/16069 const numsOrNull = [1, 2, 3, 4, null]; @@ -298,9 +296,6 @@ inferTypePredicates.ts(252,7): error TS2322: Type 'string | number | Date' is no declare let snd: string | number | Date; if (assertAndPredicate(snd)) { let t: string = snd; // should error - ~ -!!! error TS2322: Type 'string | number | Date' is not assignable to type 'string'. -!!! error TS2322: Type 'number' is not assignable to type 'string'. } function isNumberWithThis(this: Date, x: number | string) { diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 3e8cfb62bc5..69fb011dea2 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -661,7 +661,7 @@ function guardsOneButNotOthers(a: string|number, b: string|number, c: string|num // String escaping issue (please help!) function dunderguard(__x: number | string) { ->dunderguard : (__x: number | string) => ___x is string +>dunderguard : (__x: number | string) => __x is string >__x : string | number return typeof __x === 'string'; @@ -886,7 +886,7 @@ if (isNumOrStr(unk)) { // A function can be a type predicate even if it throws. function assertAndPredicate(x: string | number | Date) { ->assertAndPredicate : (x: string | number | Date) => boolean +>assertAndPredicate : (x: string | number | Date) => x is string >x : string | number | Date if (x instanceof Date) { @@ -910,12 +910,12 @@ declare let snd: string | number | Date; if (assertAndPredicate(snd)) { >assertAndPredicate(snd) : boolean ->assertAndPredicate : (x: string | number | Date) => boolean +>assertAndPredicate : (x: string | number | Date) => x is string >snd : string | number | Date let t: string = snd; // should error >t : string ->snd : string | number | Date +>snd : string } function isNumberWithThis(this: Date, x: number | string) {