Add secondary subtype check and tests

This commit is contained in:
Dan Vanderkam
2024-02-21 16:16:15 -05:00
parent e2684f1289
commit 101df932d5
6 changed files with 223 additions and 4 deletions

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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
}