diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c95eeb18287..69e904183e6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37440,14 +37440,19 @@ 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); + const trueType = checkIfExpressionRefinesParameter(expr, param, initType, /*forceFullCheck*/ false); if (trueType) { - return [i, 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]; + } } }); } - function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { + function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type, forceFullCheck: boolean): Type | undefined { const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode ?? { flags: FlowFlags.Start }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, @@ -37456,7 +37461,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType) return undefined; + if (trueType === initType && !forceFullCheck) 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. diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index ae38684f9f9..b170897623e 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -254,4 +254,25 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { return typeof x === 'number'; } + + // The type predicate must remain valid when the function is called with subtypes. + function isShortString(x: unknown) { + return typeof x === "string" && x.length < 10; + } + + declare let str: string; + if (isShortString(str)) { + str.charAt(0); // should ok + } else { + str.charAt(0); // should ok + } + + function isStringFromUnknown(x: unknown) { + return typeof x === "string"; + } + if (isStringFromUnknown(str)) { + str.charAt(0); // should OK + } else { + let t: never = str; // should OK + } \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index 0f4b992740c..d8ebad0a117 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -211,6 +211,27 @@ if (c.isC2()) { function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { return typeof x === 'number'; } + +// The type predicate must remain valid when the function is called with subtypes. +function isShortString(x: unknown) { + return typeof x === "string" && x.length < 10; +} + +declare let str: string; +if (isShortString(str)) { + str.charAt(0); // should ok +} else { + str.charAt(0); // should ok +} + +function isStringFromUnknown(x: unknown) { + return typeof x === "string"; +} +if (isStringFromUnknown(str)) { + str.charAt(0); // should OK +} else { + let t: never = str; // should OK +} //// [inferTypePredicates.js] @@ -403,3 +424,22 @@ function doNotRefineDestructuredParam(_a) { var x = _a.x, y = _a.y; return typeof x === 'number'; } +// The type predicate must remain valid when the function is called with subtypes. +function isShortString(x) { + return typeof x === "string" && x.length < 10; +} +if (isShortString(str)) { + str.charAt(0); // should ok +} +else { + str.charAt(0); // should ok +} +function isStringFromUnknown(x) { + return typeof x === "string"; +} +if (isStringFromUnknown(str)) { + str.charAt(0); // should OK +} +else { + var t = str; // should OK +} diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index cd633ff145c..778ac6a6d3d 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -606,3 +606,56 @@ function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { >x : Symbol(x, Decl(inferTypePredicates.ts, 207, 39)) } +// The type predicate must remain valid when the function is called with subtypes. +function isShortString(x: unknown) { +>isShortString : Symbol(isShortString, Decl(inferTypePredicates.ts, 209, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 212, 23)) + + return typeof x === "string" && x.length < 10; +>x : Symbol(x, Decl(inferTypePredicates.ts, 212, 23)) +>x.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 212, 23)) +>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +} + +declare let str: string; +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) + +if (isShortString(str)) { +>isShortString : Symbol(isShortString, Decl(inferTypePredicates.ts, 209, 1)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) + + str.charAt(0); // should ok +>str.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) +>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) + +} else { + str.charAt(0); // should ok +>str.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) +>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +} + +function isStringFromUnknown(x: unknown) { +>isStringFromUnknown : Symbol(isStringFromUnknown, Decl(inferTypePredicates.ts, 221, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 223, 29)) + + return typeof x === "string"; +>x : Symbol(x, Decl(inferTypePredicates.ts, 223, 29)) +} +if (isStringFromUnknown(str)) { +>isStringFromUnknown : Symbol(isStringFromUnknown, Decl(inferTypePredicates.ts, 221, 1)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) + + str.charAt(0); // should OK +>str.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) +>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) + +} else { + let t: never = str; // should OK +>t : Symbol(t, Decl(inferTypePredicates.ts, 229, 5)) +>str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) +} + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 8764f712cba..7d90fb1f0b4 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -784,3 +784,73 @@ function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { >'number' : "number" } +// The type predicate must remain valid when the function is called with subtypes. +function isShortString(x: unknown) { +>isShortString : (x: unknown) => boolean +>x : unknown + + return typeof x === "string" && x.length < 10; +>typeof x === "string" && x.length < 10 : boolean +>typeof x === "string" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>"string" : "string" +>x.length < 10 : boolean +>x.length : number +>x : string +>length : number +>10 : 10 +} + +declare let str: string; +>str : string + +if (isShortString(str)) { +>isShortString(str) : boolean +>isShortString : (x: unknown) => boolean +>str : string + + str.charAt(0); // should ok +>str.charAt(0) : string +>str.charAt : (pos: number) => string +>str : string +>charAt : (pos: number) => string +>0 : 0 + +} else { + str.charAt(0); // should ok +>str.charAt(0) : string +>str.charAt : (pos: number) => string +>str : string +>charAt : (pos: number) => string +>0 : 0 +} + +function isStringFromUnknown(x: unknown) { +>isStringFromUnknown : (x: unknown) => x is string +>x : unknown + + return typeof x === "string"; +>typeof x === "string" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>"string" : "string" +} +if (isStringFromUnknown(str)) { +>isStringFromUnknown(str) : boolean +>isStringFromUnknown : (x: unknown) => x is string +>str : string + + str.charAt(0); // should OK +>str.charAt(0) : string +>str.charAt : (pos: number) => string +>str : string +>charAt : (pos: number) => string +>0 : 0 + +} else { + let t: never = str; // should OK +>t : never +>str : never +} + diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index a4ca8136abd..519701c9d6b 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -209,3 +209,33 @@ if (c.isC2()) { function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { return typeof x === 'number'; } + +// The type predicate must remain valid when the function is called with subtypes. +function isShortString(x: unknown) { + return typeof x === "string" && x.length < 10; +} + +declare let str: string; +if (isShortString(str)) { + str.charAt(0); // should ok +} else { + str.charAt(0); // should ok +} + +function isStringFromUnknown(x: unknown) { + return typeof x === "string"; +} +if (isStringFromUnknown(str)) { + str.charAt(0); // should OK +} else { + let t: never = str; // should OK +} + +// infer a union type +function isNumOrStr(x: unknown) { + return (typeof x === "number" || typeof x === "string"); +} +declare let unk: unknown; +if (isNumOrStr(unk)) { + let t: number | string = unk; // should ok +}