diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 18fc82e8f57..307b0850392 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37411,6 +37411,7 @@ 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 } @@ -37419,13 +37420,12 @@ 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; } - if (isTriviallyNonBoolean(singleReturn)) return undefined; - const predicate = checkIfExpressionRefinesAnyParameter(singleReturn); if (predicate) { const [i, type] = predicate; @@ -37439,8 +37439,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined { expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - const type = checkExpressionCached(expr, CheckMode.TypeOnly); - if (type !== booleanType || !func.body) return undefined; + const type = checkExpressionCached(expr); + if (type !== booleanType) return undefined; return forEach(func.parameters, (param, i) => { const initType = getSymbolLinks(param.symbol).type; @@ -37448,19 +37448,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Refining "x: boolean" to "x is true" or "x is false" isn't useful. return; } - const trueType = checkIfExpressionRefinesParameter(expr, param, initType, /*forceFullCheck*/ false); + const trueType = checkIfExpressionRefinesParameter(expr, param, initType); if (trueType) { - // A type predicate would be valid if the function were called with param of type initType. - // The predicate must also be valid for all subtypes of initType. In particular, it must be valid when called with param of type trueType. - const trueSubtype = checkIfExpressionRefinesParameter(expr, param, trueType, /*forceFullCheck*/ true); - if (trueSubtype) { - return [i, trueType]; - } + return [i, trueType]; } }); } - function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type, forceFullCheck: boolean): Type | undefined { + function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode ?? { flags: FlowFlags.Start }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, @@ -37469,36 +37464,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType && !forceFullCheck) return undefined; + if (trueType === initType) return undefined; // "x is T" means that x is T if and only if it returns true. If it returns false then x is not T. - // However, TS may not be able to represent "not T", in which case we can be more lax. - // It's safe to infer a type guard if falseType = Exclude - // This matches what you'd get if you called the type guard in an if/else statement. + // 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`. const falseCondition: FlowCondition = { ...trueCondition, flags: FlowFlags.FalseCondition, }; - const falseType = getFlowTypeOfReference(param.name, initType, initType, func, falseCondition); - const candidateFalse = filterType(initType, t => !isTypeSubtypeOf(t, trueType)); - if (isTypeIdenticalTo(candidateFalse, falseType)) { - return trueType; - } - } + const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); + if (!isTypeIdenticalTo(falseSubtype, neverType)) return undefined; - // This bypasses the call to checkExpression for expressions that are clearly not booleans. - // In addition to potentially saving work, this avoids some circularlity issues. - function isTriviallyNonBoolean(expr: Expression): boolean { - if (isLiteralExpression(expr) || isLiteralExpressionOfObject(expr)) { - return true; - } - if (isIdentifier(expr)) { - const sym = getResolvedSymbol(expr); - if (sym.flags & (SymbolFlags.Class | SymbolFlags.ObjectLiteral | SymbolFlags.Function | SymbolFlags.Enum | SymbolFlags.EnumMember)) { - return true; + // 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 false; // may or may not be boolean + + return trueType; } } diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index b8ef2508d3b..e9cf945b5bd 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -14,9 +14,11 @@ 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 (10 errors) ==== +==== inferTypePredicates.ts (11 errors) ==== // https://github.com/microsoft/TypeScript/issues/16069 const numsOrNull = [1, 2, 3, 4, null]; @@ -214,8 +216,8 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 // could infer a type guard here but it doesn't seem that helpful. const booleanIdentity = (x: boolean) => x; - // could infer "x is number | true" but don't; debateable whether that's helpful. - const numOrBoolean = (x: number | boolean) => typeof x !== 'number' && x; + // we infer "x is number | true" which is accurate of debatable utility. + const numOrBoolean = (x: number | boolean) => typeof x === 'number' || x; // inferred guards in methods interface NumberInferrer { @@ -295,8 +297,13 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 declare let snd: string | number | Date; if (assertAndPredicate(snd)) { - let t: string = snd; // should ok - } else { - snd; // type is number | Date + 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) { + return typeof x === 'number'; } \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index 94b5ed4d8ad..57bb817194b 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -174,8 +174,8 @@ function dunderguard(__x: number | string) { // could infer a type guard here but it doesn't seem that helpful. const booleanIdentity = (x: boolean) => x; -// could infer "x is number | true" but don't; debateable whether that's helpful. -const numOrBoolean = (x: number | boolean) => typeof x !== 'number' && x; +// we infer "x is number | true" which is accurate of debatable utility. +const numOrBoolean = (x: number | boolean) => typeof x === 'number' || x; // inferred guards in methods interface NumberInferrer { @@ -252,9 +252,11 @@ function assertAndPredicate(x: string | number | Date) { declare let snd: string | number | Date; if (assertAndPredicate(snd)) { - let t: string = snd; // should ok -} else { - snd; // type is number | Date + let t: string = snd; // should error +} + +function isNumberWithThis(this: Date, x: number | string) { + return typeof x === 'number'; } @@ -406,8 +408,8 @@ function dunderguard(__x) { } // could infer a type guard here but it doesn't seem that helpful. var booleanIdentity = function (x) { return x; }; -// could infer "x is number | true" but don't; debateable whether that's helpful. -var numOrBoolean = function (x) { return typeof x !== 'number' && x; }; +// we infer "x is number | true" which is accurate of debatable utility. +var numOrBoolean = function (x) { return typeof x === 'number' || x; }; var Inferrer = /** @class */ (function () { function Inferrer() { } @@ -482,8 +484,8 @@ function assertAndPredicate(x) { return typeof x === 'string'; } if (assertAndPredicate(snd)) { - var t = snd; // should ok + var t = snd; // should error } -else { - snd; // type is number | Date +function isNumberWithThis(x) { + return typeof x === 'number'; } diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index adb871430b4..b8678864296 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -510,8 +510,8 @@ const booleanIdentity = (x: boolean) => x; >x : Symbol(x, Decl(inferTypePredicates.ts, 171, 25)) >x : Symbol(x, Decl(inferTypePredicates.ts, 171, 25)) -// could infer "x is number | true" but don't; debateable whether that's helpful. -const numOrBoolean = (x: number | boolean) => typeof x !== 'number' && x; +// we infer "x is number | true" which is accurate of debatable utility. +const numOrBoolean = (x: number | boolean) => typeof x === 'number' || x; >numOrBoolean : Symbol(numOrBoolean, Decl(inferTypePredicates.ts, 174, 5)) >x : Symbol(x, Decl(inferTypePredicates.ts, 174, 22)) >x : Symbol(x, Decl(inferTypePredicates.ts, 174, 22)) @@ -705,12 +705,18 @@ if (assertAndPredicate(snd)) { >assertAndPredicate : Symbol(assertAndPredicate, Decl(inferTypePredicates.ts, 239, 1)) >snd : Symbol(snd, Decl(inferTypePredicates.ts, 249, 11)) - let t: string = snd; // should ok + let t: string = snd; // should error >t : Symbol(t, Decl(inferTypePredicates.ts, 251, 5)) >snd : Symbol(snd, Decl(inferTypePredicates.ts, 249, 11)) - -} else { - snd; // type is number | Date ->snd : Symbol(snd, Decl(inferTypePredicates.ts, 249, 11)) +} + +function isNumberWithThis(this: Date, x: number | string) { +>isNumberWithThis : Symbol(isNumberWithThis, Decl(inferTypePredicates.ts, 252, 1)) +>this : Symbol(this, Decl(inferTypePredicates.ts, 254, 26)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 254, 37)) + + return typeof x === 'number'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 254, 37)) } diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 27206d90209..3e8cfb62bc5 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -678,13 +678,13 @@ const booleanIdentity = (x: boolean) => x; >x : boolean >x : boolean -// could infer "x is number | true" but don't; debateable whether that's helpful. -const numOrBoolean = (x: number | boolean) => typeof x !== 'number' && x; ->numOrBoolean : (x: number | boolean) => x is true ->(x: number | boolean) => typeof x !== 'number' && x : (x: number | boolean) => x is true +// we infer "x is number | true" which is accurate of debatable utility. +const numOrBoolean = (x: number | boolean) => typeof x === 'number' || x; +>numOrBoolean : (x: number | boolean) => x is number | true +>(x: number | boolean) => typeof x === 'number' || x : (x: number | boolean) => x is number | true >x : number | boolean ->typeof x !== 'number' && x : boolean ->typeof x !== 'number' : boolean +>typeof x === 'number' || x : boolean +>typeof x === 'number' : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" >x : number | boolean >'number' : "number" @@ -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) => x is string +>assertAndPredicate : (x: string | number | Date) => boolean >x : string | number | Date if (x instanceof Date) { @@ -910,15 +910,23 @@ declare let snd: string | number | Date; if (assertAndPredicate(snd)) { >assertAndPredicate(snd) : boolean ->assertAndPredicate : (x: string | number | Date) => x is string +>assertAndPredicate : (x: string | number | Date) => boolean >snd : string | number | Date - let t: string = snd; // should ok + let t: string = snd; // should error >t : string ->snd : string - -} else { - snd; // type is number | Date ->snd : number | Date +>snd : string | number | Date +} + +function isNumberWithThis(this: Date, x: number | string) { +>isNumberWithThis : (this: Date, x: number | string) => x is number +>this : Date +>x : string | number + + return typeof x === 'number'; +>typeof x === 'number' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'number' : "number" } diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index 388f38910f5..42fdbccf1a4 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -172,8 +172,8 @@ function dunderguard(__x: number | string) { // could infer a type guard here but it doesn't seem that helpful. const booleanIdentity = (x: boolean) => x; -// could infer "x is number | true" but don't; debateable whether that's helpful. -const numOrBoolean = (x: number | boolean) => typeof x !== 'number' && x; +// we infer "x is number | true" which is accurate of debatable utility. +const numOrBoolean = (x: number | boolean) => typeof x === 'number' || x; // inferred guards in methods interface NumberInferrer { @@ -250,7 +250,9 @@ function assertAndPredicate(x: string | number | Date) { declare let snd: string | number | Date; if (assertAndPredicate(snd)) { - let t: string = snd; // should ok -} else { - snd; // type is number | Date + let t: string = snd; // should error +} + +function isNumberWithThis(this: Date, x: number | string) { + return typeof x === 'number'; }