mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-04-17 01:49:41 -05:00
Drop isTriviallyNonBoolean, switch to simpler test, check for assertions
This commit is contained in:
@@ -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<initType, trueType>
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user