From 0c24ccc049cf0b9bbac8a0ac4f3debe768b5c1ca Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 20 Feb 2024 17:32:46 -0500 Subject: [PATCH 01/27] Infer type predicates from function bodies --- src/compiler/checker.ts | 88 +- .../reference/findLast(target=esnext).types | 66 +- .../reference/importHelpersES6.types | 2 +- .../reference/inKeywordAndUnknown.types | 12 +- .../reference/inferTypePredicates.errors.txt | 257 ++++++ .../reference/inferTypePredicates.js | 405 +++++++++ .../reference/inferTypePredicates.symbols | 608 ++++++++++++++ .../reference/inferTypePredicates.types | 786 ++++++++++++++++++ ...riptThisAssignmentInStaticBlock.errors.txt | 30 - ...avascriptThisAssignmentInStaticBlock.types | 16 +- .../reference/narrowByInstanceof.types | 2 +- tests/cases/compiler/inferTypePredicates.ts | 211 +++++ .../thisPredicateFunctionQuickInfo.ts | 2 +- 13 files changed, 2402 insertions(+), 83 deletions(-) create mode 100644 tests/baselines/reference/inferTypePredicates.errors.txt create mode 100644 tests/baselines/reference/inferTypePredicates.js create mode 100644 tests/baselines/reference/inferTypePredicates.symbols create mode 100644 tests/baselines/reference/inferTypePredicates.types delete mode 100644 tests/baselines/reference/javascriptThisAssignmentInStaticBlock.errors.txt create mode 100644 tests/cases/compiler/inferTypePredicates.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f9ab74b053256..6d75df43a1d3b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15458,9 +15458,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { jsdocPredicate = getTypePredicateOfSignature(jsdocSignature); } } - signature.resolvedTypePredicate = type && isTypePredicateNode(type) ? - createTypePredicateFromTypePredicateNode(type, signature) : - jsdocPredicate || noTypePredicate; + if (type || jsdocPredicate) { + signature.resolvedTypePredicate = type && isTypePredicateNode(type) ? + createTypePredicateFromTypePredicateNode(type, signature) : + jsdocPredicate || noTypePredicate; + } else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) { + const {declaration} = signature; + signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop + signature.resolvedTypePredicate = getTypePredicateFromBody(declaration, signature) || noTypePredicate; + } else { + signature.resolvedTypePredicate = noTypePredicate; + } } Debug.assert(!!signature.resolvedTypePredicate); } @@ -37389,6 +37397,80 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } + function getTypePredicateFromBody(func: FunctionLikeDeclaration, _sig: Signature): TypePredicate | undefined { + const functionFlags = getFunctionFlags(func); + if (functionFlags !== FunctionFlags.Normal) return undefined; + + // Only attempt to infer a type predicate if there's exactly one return. + let singleReturn: Expression | undefined; + if (func.body && func.body.kind !== SyntaxKind.Block) { + singleReturn = func.body; // arrow function + } else { + if (functionHasImplicitReturn(func)) return undefined; + + const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => { + if (singleReturn || !returnStatement.expression) return true; + singleReturn = returnStatement.expression; + }); + if (bailedEarly || !singleReturn) return undefined; + } + + const predicate = checkIfExpressionRefinesAnyParameter(singleReturn); + if (predicate) { + 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 undefined; + + 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; + + return forEach(func.parameters, (param, i) => { + const initType = getSymbolLinks(param.symbol).type; + if (!initType || initType === booleanType || isSymbolAssigned(param.symbol)) { + // Refining "x: boolean" to "x is true" or "x is false" isn't useful. + return; + } + const trueType = checkIfExpressionRefinesParameter(expr, param, initType); + if (trueType) { + return [i, trueType]; + } + }); + } + + 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, + node: expr, + antecedent, + }; + + const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); + 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. + 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; + } + } + } + /** * TypeScript Specification 1.0 (6.3) - July 2014 * An explicitly typed function whose return type isn't the Void type, diff --git a/tests/baselines/reference/findLast(target=esnext).types b/tests/baselines/reference/findLast(target=esnext).types index cd390c3a57f3b..d010b65027bab 100644 --- a/tests/baselines/reference/findLast(target=esnext).types +++ b/tests/baselines/reference/findLast(target=esnext).types @@ -3,12 +3,12 @@ === findLast.ts === const itemNumber: number | undefined = [0].findLast((item) => item === 0); >itemNumber : number ->[0].findLast((item) => item === 0) : number +>[0].findLast((item) => item === 0) : 0 >[0].findLast : { (predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number; } >[0] : number[] >0 : 0 >findLast : { (predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -16,120 +16,120 @@ const itemNumber: number | undefined = [0].findLast((item) => item === 0); const itemString: string | undefined = ["string"].findLast((item) => item === "string"); >itemString : string ->["string"].findLast((item) => item === "string") : string +>["string"].findLast((item) => item === "string") : "string" >["string"].findLast : { (predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S; (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string; } >["string"] : string[] >"string" : "string" >findLast : { (predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S; (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string; } ->(item) => item === "string" : (item: string) => boolean +>(item) => item === "string" : (item: string) => item is "string" >item : string >item === "string" : boolean >item : string >"string" : "string" new Int8Array().findLast((item) => item === 0); ->new Int8Array().findLast((item) => item === 0) : number +>new Int8Array().findLast((item) => item === 0) : 0 >new Int8Array().findLast : { (predicate: (value: number, index: number, array: Int8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): number; } >new Int8Array() : Int8Array >Int8Array : Int8ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Int8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Uint8Array().findLast((item) => item === 0); ->new Uint8Array().findLast((item) => item === 0) : number +>new Uint8Array().findLast((item) => item === 0) : 0 >new Uint8Array().findLast : { (predicate: (value: number, index: number, array: Uint8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): number; } >new Uint8Array() : Uint8Array >Uint8Array : Uint8ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Uint8Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Uint8ClampedArray().findLast((item) => item === 0); ->new Uint8ClampedArray().findLast((item) => item === 0) : number +>new Uint8ClampedArray().findLast((item) => item === 0) : 0 >new Uint8ClampedArray().findLast : { (predicate: (value: number, index: number, array: Uint8ClampedArray) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): number; } >new Uint8ClampedArray() : Uint8ClampedArray >Uint8ClampedArray : Uint8ClampedArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Uint8ClampedArray) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Int16Array().findLast((item) => item === 0); ->new Int16Array().findLast((item) => item === 0) : number +>new Int16Array().findLast((item) => item === 0) : 0 >new Int16Array().findLast : { (predicate: (value: number, index: number, array: Int16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): number; } >new Int16Array() : Int16Array >Int16Array : Int16ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Int16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Uint16Array().findLast((item) => item === 0); ->new Uint16Array().findLast((item) => item === 0) : number +>new Uint16Array().findLast((item) => item === 0) : 0 >new Uint16Array().findLast : { (predicate: (value: number, index: number, array: Uint16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): number; } >new Uint16Array() : Uint16Array >Uint16Array : Uint16ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Uint16Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Int32Array().findLast((item) => item === 0); ->new Int32Array().findLast((item) => item === 0) : number +>new Int32Array().findLast((item) => item === 0) : 0 >new Int32Array().findLast : { (predicate: (value: number, index: number, array: Int32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): number; } >new Int32Array() : Int32Array >Int32Array : Int32ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Int32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Uint32Array().findLast((item) => item === 0); ->new Uint32Array().findLast((item) => item === 0) : number +>new Uint32Array().findLast((item) => item === 0) : 0 >new Uint32Array().findLast : { (predicate: (value: number, index: number, array: Uint32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): number; } >new Uint32Array() : Uint32Array >Uint32Array : Uint32ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Uint32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Float32Array().findLast((item) => item === 0); ->new Float32Array().findLast((item) => item === 0) : number +>new Float32Array().findLast((item) => item === 0) : 0 >new Float32Array().findLast : { (predicate: (value: number, index: number, array: Float32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): number; } >new Float32Array() : Float32Array >Float32Array : Float32ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Float32Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number >0 : 0 new Float64Array().findLast((item) => item === 0); ->new Float64Array().findLast((item) => item === 0) : number +>new Float64Array().findLast((item) => item === 0) : 0 >new Float64Array().findLast : { (predicate: (value: number, index: number, array: Float64Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): number; } >new Float64Array() : Float64Array >Float64Array : Float64ArrayConstructor >findLast : { (predicate: (value: number, index: number, array: Float64Array) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): number; } ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -170,7 +170,7 @@ const indexNumber: number = [0].findLastIndex((item) => item === 0); >[0] : number[] >0 : 0 >findLastIndex : (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -183,7 +183,7 @@ const indexString: number = ["string"].findLastIndex((item) => item === "string" >["string"] : string[] >"string" : "string" >findLastIndex : (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any) => number ->(item) => item === "string" : (item: string) => boolean +>(item) => item === "string" : (item: string) => item is "string" >item : string >item === "string" : boolean >item : string @@ -195,7 +195,7 @@ new Int8Array().findLastIndex((item) => item === 0); >new Int8Array() : Int8Array >Int8Array : Int8ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -207,7 +207,7 @@ new Uint8Array().findLastIndex((item) => item === 0); >new Uint8Array() : Uint8Array >Uint8Array : Uint8ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -219,7 +219,7 @@ new Uint8ClampedArray().findLastIndex((item) => item === 0); >new Uint8ClampedArray() : Uint8ClampedArray >Uint8ClampedArray : Uint8ClampedArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -231,7 +231,7 @@ new Int16Array().findLastIndex((item) => item === 0); >new Int16Array() : Int16Array >Int16Array : Int16ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -243,7 +243,7 @@ new Uint16Array().findLastIndex((item) => item === 0); >new Uint16Array() : Uint16Array >Uint16Array : Uint16ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -255,7 +255,7 @@ new Int32Array().findLastIndex((item) => item === 0); >new Int32Array() : Int32Array >Int32Array : Int32ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -267,7 +267,7 @@ new Uint32Array().findLastIndex((item) => item === 0); >new Uint32Array() : Uint32Array >Uint32Array : Uint32ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -279,7 +279,7 @@ new Float32Array().findLastIndex((item) => item === 0); >new Float32Array() : Float32Array >Float32Array : Float32ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number @@ -291,7 +291,7 @@ new Float64Array().findLastIndex((item) => item === 0); >new Float64Array() : Float64Array >Float64Array : Float64ArrayConstructor >findLastIndex : (predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any) => number ->(item) => item === 0 : (item: number) => boolean +>(item) => item === 0 : (item: number) => item is 0 >item : number >item === 0 : boolean >item : number diff --git a/tests/baselines/reference/importHelpersES6.types b/tests/baselines/reference/importHelpersES6.types index 9ed54f457c130..8ae8c0cc90de0 100644 --- a/tests/baselines/reference/importHelpersES6.types +++ b/tests/baselines/reference/importHelpersES6.types @@ -22,7 +22,7 @@ declare var dec: any; >this : this g(u) { return #x in u; } ->g : (u: any) => boolean +>g : (u: any) => u is A >u : any >#x in u : boolean >#x : any diff --git a/tests/baselines/reference/inKeywordAndUnknown.types b/tests/baselines/reference/inKeywordAndUnknown.types index 81a24b272970f..577e19857d586 100644 --- a/tests/baselines/reference/inKeywordAndUnknown.types +++ b/tests/baselines/reference/inKeywordAndUnknown.types @@ -79,7 +79,7 @@ function f1(x: unknown) { } function f2(x: T) { ->f2 : (x: T) => boolean +>f2 : (x: T) => x is T & Object & Record<"a", unknown> >x : T return x && x instanceof Object && 'a' in x; @@ -95,7 +95,7 @@ function f2(x: T) { } function f3(x: {}) { ->f3 : (x: {}) => boolean +>f3 : (x: {}) => x is Object & Record<"a", unknown> >x : {} return x instanceof Object && 'a' in x; @@ -109,7 +109,7 @@ function f3(x: {}) { } function f4(x: T) { ->f4 : (x: T) => boolean +>f4 : (x: T) => x is T & Object & Record<"a", unknown> >x : T return x instanceof Object && 'a' in x; @@ -123,7 +123,7 @@ function f4(x: T) { } function f5(x: T & {}) { ->f5 : (x: T & {}) => boolean +>f5 : (x: T & {}) => x is T & Object & Record<"a", unknown> >x : T & {} return x instanceof Object && 'a' in x; @@ -137,7 +137,7 @@ function f5(x: T & {}) { } function f6(x: T & {}) { ->f6 : (x: T & {}) => boolean +>f6 : (x: T & {}) => x is T & Object & Record<"a", unknown> >x : T return x instanceof Object && 'a' in x; @@ -151,7 +151,7 @@ function f6(x: T & {}) { } function f7(x: T & {}) { ->f7 : (x: T & {}) => boolean +>f7 : (x: T & {}) => x is T & Record<"a", unknown> >x : T return x instanceof Object && 'a' in x; diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt new file mode 100644 index 0000000000000..ae38684f9f947 --- /dev/null +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -0,0 +1,257 @@ +inferTypePredicates.ts(4,7): error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. + Type 'number | null' is not assignable to type 'number'. + Type 'null' is not assignable to type 'number'. +inferTypePredicates.ts(7,7): error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. +inferTypePredicates.ts(14,7): error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. +inferTypePredicates.ts(52,17): error TS18048: 'arr' is possibly 'undefined'. +inferTypePredicates.ts(54,28): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + Type 'undefined' is not assignable to type 'string'. +inferTypePredicates.ts(65,28): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + Type 'undefined' is not assignable to type 'string'. +inferTypePredicates.ts(90,8): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. +inferTypePredicates.ts(113,7): error TS2322: Type 'string | number' is not assignable to type 'string'. + Type 'number' is not assignable to type 'string'. +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 (10 errors) ==== + // https://github.com/microsoft/TypeScript/issues/16069 + + const numsOrNull = [1, 2, 3, 4, null]; + const filteredNumsTruthy: number[] = numsOrNull.filter(x => !!x); // should error + ~~~~~~~~~~~~~~~~~~ +!!! error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. +!!! error TS2322: Type 'number | null' is not assignable to type 'number'. +!!! error TS2322: Type 'null' is not assignable to type 'number'. + const filteredNumsNonNullish: number[] = numsOrNull.filter(x => x !== null); // should ok + + const evenSquaresInline: number[] = // should error + ~~~~~~~~~~~~~~~~~ +!!! error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => !!x); // tests truthiness, not non-nullishness + + const isTruthy = (x: number | null) => !!x; + + const evenSquares: number[] = // should error + ~~~~~~~~~~~ +!!! error TS2322: Type '(number | null)[]' is not assignable to type 'number[]'. + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(isTruthy); + + const evenSquaresNonNull: number[] = // should ok + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => x !== null); + + function isNonNull(x: number | null) { + return x !== null; + } + + // factoring out a boolean works thanks to aliased discriminants + function isNonNullVar(x: number | null) { + const ok = x !== null; + return ok; + } + + function isNonNullGeneric(x: T) { + return x !== null; + } + + // Type guards can flow between functions + const myGuard = (o: string | undefined): o is string => !!o; + const mySecondGuard = (o: string | undefined) => myGuard(o); + + // https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1327449914 + // This doesn't work because the false condition prevents type guard inference. + // Breaking up the filters does work. + type MyObj = { data?: string }; + type MyArray = { list?: MyObj[] }[]; + const myArray: MyArray = []; + + const result = myArray + .map((arr) => arr.list) + .filter((arr) => arr && arr.length) + .map((arr) => arr // should error + ~~~ +!!! error TS18048: 'arr' is possibly 'undefined'. + .filter((obj) => obj && obj.data) + .map(obj => JSON.parse(obj.data)) // should error + ~~~~~~~~ +!!! error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. +!!! error TS2345: Type 'undefined' is not assignable to type 'string'. + ); + + const result2 = myArray + .map((arr) => arr.list) + .filter((arr) => !!arr) + .filter(arr => arr.length) + .map((arr) => arr // should ok + .filter((obj) => obj) + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(obj => !!obj.data) + .map(obj => JSON.parse(obj.data)) + ~~~~~~~~ +!!! error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. +!!! error TS2345: Type 'undefined' is not assignable to type 'string'. + ); + + // https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1183547889 + type Foo = { + foo: string; + } + type Bar = Foo & { + bar: string; + } + + const list: (Foo | Bar)[] = []; + const resultBars: Bar[] = list.filter((value) => 'bar' in value); // should ok + + function isBarNonNull(x: Foo | Bar | null) { + return ('bar' in x!); + } + const fooOrBar = list[0]; + if (isBarNonNull(fooOrBar)) { + const t: Bar = fooOrBar; // should ok + } + + // https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 + // Ryan's example (currently legal): + const a = [1, "foo", 2, "bar"].filter(x => typeof x === "string"); + a.push(10); + ~~ +!!! error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. + + // Defer to explicit type guards, even when they're incorrect. + function backwardsGuard(x: number|string): x is number { + return typeof x === 'string'; + } + + // Partition tests. The "false" case matters. + function isString(x: string | number) { + return typeof x === 'string'; + } + + declare let strOrNum: string | number; + if (isString(strOrNum)) { + let t: string = strOrNum; // should ok + } else { + let t: number = strOrNum; // should ok + } + + function flakyIsString(x: string | number) { + return typeof x === 'string' && Math.random() > 0.5; + } + if (flakyIsString(strOrNum)) { + let t: string = strOrNum; // should error + ~ +!!! error TS2322: Type 'string | number' is not assignable to type 'string'. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + } else { + let t: number = strOrNum; // should error + ~ +!!! error TS2322: Type 'string | number' is not assignable to type 'number'. +!!! error TS2322: Type 'string' is not assignable to type 'number'. + } + + function isDate(x: object): x is Date { + return x instanceof Date; + } + function flakyIsDate(x: object): x is Date { + return x instanceof Date; + } + + declare let maybeDate: object; + if (isDate(maybeDate)) { + let t: Date = maybeDate; // should ok + } else { + let t: object = maybeDate; // should ok + } + + if (flakyIsDate(maybeDate)) { + let t: Date = maybeDate; // should ok + } else { + let t: object = maybeDate; // should ok + } + + // This should not infer a type guard since the value on which we do the refinement + // is not related to the original parameter. + function irrelevantIsNumber(x: string | number) { + x = Math.random() < 0.5 ? "string" : 123; + return typeof x === 'string'; + } + function irrelevantIsNumberDestructuring(x: string | number) { + [x] = [Math.random() < 0.5 ? "string" : 123]; + return typeof x === 'string'; + } + + // Cannot infer a type guard for either param because of the false case. + function areBothNums(x: string|number, y: string|number) { + return typeof x === 'number' && typeof y === 'number'; + } + + // Could potentially infer a type guard here but it would require more bookkeeping. + function doubleReturn(x: string|number) { + if (typeof x === 'string') { + return true; + } + return false; + } + + function guardsOneButNotOthers(a: string|number, b: string|number, c: string|number) { + return typeof b === 'string'; + } + + // String escaping issue (please help!) + function dunderguard(__x: number | string) { + return typeof __x === '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; + + // inferred guards in methods + interface NumberInferrer { + isNumber(x: number | string): x is number; + } + class Inferrer implements NumberInferrer { + isNumber(x: number | string) { // should ok + return typeof x === 'number'; + } + } + declare let numOrStr: number | string; + const inf = new Inferrer(); + if (inf.isNumber(numOrStr)) { + let t: number = numOrStr; // should ok + } else { + let t: string = numOrStr; // should ok + } + + // Type predicates are not inferred on "this" + class C1 { + isC2() { + return this instanceof C2; + } + } + class C2 extends C1 { + z = 0; + } + declare let c: C1; + if (c.isC2()) { + let c2: C2 = c; // should error + ~~ +!!! error TS2741: Property 'z' is missing in type 'C1' but required in type 'C2'. +!!! related TS2728 inferTypePredicates.ts:201:3: 'z' is declared here. + } + + function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { + return typeof x === 'number'; + } + \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js new file mode 100644 index 0000000000000..0f4b992740c04 --- /dev/null +++ b/tests/baselines/reference/inferTypePredicates.js @@ -0,0 +1,405 @@ +//// [tests/cases/compiler/inferTypePredicates.ts] //// + +//// [inferTypePredicates.ts] +// https://github.com/microsoft/TypeScript/issues/16069 + +const numsOrNull = [1, 2, 3, 4, null]; +const filteredNumsTruthy: number[] = numsOrNull.filter(x => !!x); // should error +const filteredNumsNonNullish: number[] = numsOrNull.filter(x => x !== null); // should ok + +const evenSquaresInline: number[] = // should error + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => !!x); // tests truthiness, not non-nullishness + +const isTruthy = (x: number | null) => !!x; + +const evenSquares: number[] = // should error + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(isTruthy); + +const evenSquaresNonNull: number[] = // should ok + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => x !== null); + +function isNonNull(x: number | null) { + return x !== null; +} + +// factoring out a boolean works thanks to aliased discriminants +function isNonNullVar(x: number | null) { + const ok = x !== null; + return ok; +} + +function isNonNullGeneric(x: T) { + return x !== null; +} + +// Type guards can flow between functions +const myGuard = (o: string | undefined): o is string => !!o; +const mySecondGuard = (o: string | undefined) => myGuard(o); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1327449914 +// This doesn't work because the false condition prevents type guard inference. +// Breaking up the filters does work. +type MyObj = { data?: string }; +type MyArray = { list?: MyObj[] }[]; +const myArray: MyArray = []; + +const result = myArray + .map((arr) => arr.list) + .filter((arr) => arr && arr.length) + .map((arr) => arr // should error + .filter((obj) => obj && obj.data) + .map(obj => JSON.parse(obj.data)) // should error + ); + +const result2 = myArray + .map((arr) => arr.list) + .filter((arr) => !!arr) + .filter(arr => arr.length) + .map((arr) => arr // should ok + .filter((obj) => obj) + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(obj => !!obj.data) + .map(obj => JSON.parse(obj.data)) + ); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1183547889 +type Foo = { + foo: string; +} +type Bar = Foo & { + bar: string; +} + +const list: (Foo | Bar)[] = []; +const resultBars: Bar[] = list.filter((value) => 'bar' in value); // should ok + +function isBarNonNull(x: Foo | Bar | null) { + return ('bar' in x!); +} +const fooOrBar = list[0]; +if (isBarNonNull(fooOrBar)) { + const t: Bar = fooOrBar; // should ok +} + +// https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 +// Ryan's example (currently legal): +const a = [1, "foo", 2, "bar"].filter(x => typeof x === "string"); +a.push(10); + +// Defer to explicit type guards, even when they're incorrect. +function backwardsGuard(x: number|string): x is number { + return typeof x === 'string'; +} + +// Partition tests. The "false" case matters. +function isString(x: string | number) { + return typeof x === 'string'; +} + +declare let strOrNum: string | number; +if (isString(strOrNum)) { + let t: string = strOrNum; // should ok +} else { + let t: number = strOrNum; // should ok +} + +function flakyIsString(x: string | number) { + return typeof x === 'string' && Math.random() > 0.5; +} +if (flakyIsString(strOrNum)) { + let t: string = strOrNum; // should error +} else { + let t: number = strOrNum; // should error +} + +function isDate(x: object): x is Date { + return x instanceof Date; +} +function flakyIsDate(x: object): x is Date { + return x instanceof Date; +} + +declare let maybeDate: object; +if (isDate(maybeDate)) { + let t: Date = maybeDate; // should ok +} else { + let t: object = maybeDate; // should ok +} + +if (flakyIsDate(maybeDate)) { + let t: Date = maybeDate; // should ok +} else { + let t: object = maybeDate; // should ok +} + +// This should not infer a type guard since the value on which we do the refinement +// is not related to the original parameter. +function irrelevantIsNumber(x: string | number) { + x = Math.random() < 0.5 ? "string" : 123; + return typeof x === 'string'; +} +function irrelevantIsNumberDestructuring(x: string | number) { + [x] = [Math.random() < 0.5 ? "string" : 123]; + return typeof x === 'string'; +} + +// Cannot infer a type guard for either param because of the false case. +function areBothNums(x: string|number, y: string|number) { + return typeof x === 'number' && typeof y === 'number'; +} + +// Could potentially infer a type guard here but it would require more bookkeeping. +function doubleReturn(x: string|number) { + if (typeof x === 'string') { + return true; + } + return false; +} + +function guardsOneButNotOthers(a: string|number, b: string|number, c: string|number) { + return typeof b === 'string'; +} + +// String escaping issue (please help!) +function dunderguard(__x: number | string) { + return typeof __x === '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; + +// inferred guards in methods +interface NumberInferrer { + isNumber(x: number | string): x is number; +} +class Inferrer implements NumberInferrer { + isNumber(x: number | string) { // should ok + return typeof x === 'number'; + } +} +declare let numOrStr: number | string; +const inf = new Inferrer(); +if (inf.isNumber(numOrStr)) { + let t: number = numOrStr; // should ok +} else { + let t: string = numOrStr; // should ok +} + +// Type predicates are not inferred on "this" +class C1 { + isC2() { + return this instanceof C2; + } +} +class C2 extends C1 { + z = 0; +} +declare let c: C1; +if (c.isC2()) { + let c2: C2 = c; // should error +} + +function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { + return typeof x === 'number'; +} + + +//// [inferTypePredicates.js] +// https://github.com/microsoft/TypeScript/issues/16069 +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var numsOrNull = [1, 2, 3, 4, null]; +var filteredNumsTruthy = numsOrNull.filter(function (x) { return !!x; }); // should error +var filteredNumsNonNullish = numsOrNull.filter(function (x) { return x !== null; }); // should ok +var evenSquaresInline = // should error + [1, 2, 3, 4] + .map(function (x) { return x % 2 === 0 ? x * x : null; }) + .filter(function (x) { return !!x; }); // tests truthiness, not non-nullishness +var isTruthy = function (x) { return !!x; }; +var evenSquares = // should error + [1, 2, 3, 4] + .map(function (x) { return x % 2 === 0 ? x * x : null; }) + .filter(isTruthy); +var evenSquaresNonNull = // should ok + [1, 2, 3, 4] + .map(function (x) { return x % 2 === 0 ? x * x : null; }) + .filter(function (x) { return x !== null; }); +function isNonNull(x) { + return x !== null; +} +// factoring out a boolean works thanks to aliased discriminants +function isNonNullVar(x) { + var ok = x !== null; + return ok; +} +function isNonNullGeneric(x) { + return x !== null; +} +// Type guards can flow between functions +var myGuard = function (o) { return !!o; }; +var mySecondGuard = function (o) { return myGuard(o); }; +var myArray = []; +var result = myArray + .map(function (arr) { return arr.list; }) + .filter(function (arr) { return arr && arr.length; }) + .map(function (arr) { return arr // should error + .filter(function (obj) { return obj && obj.data; }) + .map(function (obj) { return JSON.parse(obj.data); }); } // should error +); +var result2 = myArray + .map(function (arr) { return arr.list; }) + .filter(function (arr) { return !!arr; }) + .filter(function (arr) { return arr.length; }) + .map(function (arr) { return arr // should ok + .filter(function (obj) { return obj; }) + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(function (obj) { return !!obj.data; }) + .map(function (obj) { return JSON.parse(obj.data); }); }); +var list = []; +var resultBars = list.filter(function (value) { return 'bar' in value; }); // should ok +function isBarNonNull(x) { + return ('bar' in x); +} +var fooOrBar = list[0]; +if (isBarNonNull(fooOrBar)) { + var t = fooOrBar; // should ok +} +// https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 +// Ryan's example (currently legal): +var a = [1, "foo", 2, "bar"].filter(function (x) { return typeof x === "string"; }); +a.push(10); +// Defer to explicit type guards, even when they're incorrect. +function backwardsGuard(x) { + return typeof x === 'string'; +} +// Partition tests. The "false" case matters. +function isString(x) { + return typeof x === 'string'; +} +if (isString(strOrNum)) { + var t = strOrNum; // should ok +} +else { + var t = strOrNum; // should ok +} +function flakyIsString(x) { + return typeof x === 'string' && Math.random() > 0.5; +} +if (flakyIsString(strOrNum)) { + var t = strOrNum; // should error +} +else { + var t = strOrNum; // should error +} +function isDate(x) { + return x instanceof Date; +} +function flakyIsDate(x) { + return x instanceof Date; +} +if (isDate(maybeDate)) { + var t = maybeDate; // should ok +} +else { + var t = maybeDate; // should ok +} +if (flakyIsDate(maybeDate)) { + var t = maybeDate; // should ok +} +else { + var t = maybeDate; // should ok +} +// This should not infer a type guard since the value on which we do the refinement +// is not related to the original parameter. +function irrelevantIsNumber(x) { + x = Math.random() < 0.5 ? "string" : 123; + return typeof x === 'string'; +} +function irrelevantIsNumberDestructuring(x) { + x = [Math.random() < 0.5 ? "string" : 123][0]; + return typeof x === 'string'; +} +// Cannot infer a type guard for either param because of the false case. +function areBothNums(x, y) { + return typeof x === 'number' && typeof y === 'number'; +} +// Could potentially infer a type guard here but it would require more bookkeeping. +function doubleReturn(x) { + if (typeof x === 'string') { + return true; + } + return false; +} +function guardsOneButNotOthers(a, b, c) { + return typeof b === 'string'; +} +// String escaping issue (please help!) +function dunderguard(__x) { + return typeof __x === 'string'; +} +// 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; }; +var Inferrer = /** @class */ (function () { + function Inferrer() { + } + Inferrer.prototype.isNumber = function (x) { + return typeof x === 'number'; + }; + return Inferrer; +}()); +var inf = new Inferrer(); +if (inf.isNumber(numOrStr)) { + var t = numOrStr; // should ok +} +else { + var t = numOrStr; // should ok +} +// Type predicates are not inferred on "this" +var C1 = /** @class */ (function () { + function C1() { + } + C1.prototype.isC2 = function () { + return this instanceof C2; + }; + return C1; +}()); +var C2 = /** @class */ (function (_super) { + __extends(C2, _super); + function C2() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.z = 0; + return _this; + } + return C2; +}(C1)); +if (c.isC2()) { + var c2 = c; // should error +} +function doNotRefineDestructuredParam(_a) { + var x = _a.x, y = _a.y; + return typeof x === 'number'; +} diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols new file mode 100644 index 0000000000000..cd633ff145cda --- /dev/null +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -0,0 +1,608 @@ +//// [tests/cases/compiler/inferTypePredicates.ts] //// + +=== inferTypePredicates.ts === +// https://github.com/microsoft/TypeScript/issues/16069 + +const numsOrNull = [1, 2, 3, 4, null]; +>numsOrNull : Symbol(numsOrNull, Decl(inferTypePredicates.ts, 2, 5)) + +const filteredNumsTruthy: number[] = numsOrNull.filter(x => !!x); // should error +>filteredNumsTruthy : Symbol(filteredNumsTruthy, Decl(inferTypePredicates.ts, 3, 5)) +>numsOrNull.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>numsOrNull : Symbol(numsOrNull, Decl(inferTypePredicates.ts, 2, 5)) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 3, 55)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 3, 55)) + +const filteredNumsNonNullish: number[] = numsOrNull.filter(x => x !== null); // should ok +>filteredNumsNonNullish : Symbol(filteredNumsNonNullish, Decl(inferTypePredicates.ts, 4, 5)) +>numsOrNull.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>numsOrNull : Symbol(numsOrNull, Decl(inferTypePredicates.ts, 2, 5)) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 4, 59)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 4, 59)) + +const evenSquaresInline: number[] = // should error +>evenSquaresInline : Symbol(evenSquaresInline, Decl(inferTypePredicates.ts, 6, 5)) + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>[1, 2, 3, 4] .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) + + .map(x => x % 2 === 0 ? x * x : null) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 8, 13)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 8, 13)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 8, 13)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 8, 13)) + + .filter(x => !!x); // tests truthiness, not non-nullishness +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 9, 16)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 9, 16)) + +const isTruthy = (x: number | null) => !!x; +>isTruthy : Symbol(isTruthy, Decl(inferTypePredicates.ts, 11, 5)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 11, 18)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 11, 18)) + +const evenSquares: number[] = // should error +>evenSquares : Symbol(evenSquares, Decl(inferTypePredicates.ts, 13, 5)) + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>[1, 2, 3, 4] .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) + + .map(x => x % 2 === 0 ? x * x : null) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 15, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 15, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 15, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 15, 9)) + + .filter(isTruthy); +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>isTruthy : Symbol(isTruthy, Decl(inferTypePredicates.ts, 11, 5)) + +const evenSquaresNonNull: number[] = // should ok +>evenSquaresNonNull : Symbol(evenSquaresNonNull, Decl(inferTypePredicates.ts, 18, 5)) + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>[1, 2, 3, 4] .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) + + .map(x => x % 2 === 0 ? x * x : null) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 20, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 20, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 20, 9)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 20, 9)) + + .filter(x => x !== null); +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 21, 12)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 21, 12)) + +function isNonNull(x: number | null) { +>isNonNull : Symbol(isNonNull, Decl(inferTypePredicates.ts, 21, 29)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 23, 19)) + + return x !== null; +>x : Symbol(x, Decl(inferTypePredicates.ts, 23, 19)) +} + +// factoring out a boolean works thanks to aliased discriminants +function isNonNullVar(x: number | null) { +>isNonNullVar : Symbol(isNonNullVar, Decl(inferTypePredicates.ts, 25, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 28, 22)) + + const ok = x !== null; +>ok : Symbol(ok, Decl(inferTypePredicates.ts, 29, 7)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 28, 22)) + + return ok; +>ok : Symbol(ok, Decl(inferTypePredicates.ts, 29, 7)) +} + +function isNonNullGeneric(x: T) { +>isNonNullGeneric : Symbol(isNonNullGeneric, Decl(inferTypePredicates.ts, 31, 1)) +>T : Symbol(T, Decl(inferTypePredicates.ts, 33, 26)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 33, 29)) +>T : Symbol(T, Decl(inferTypePredicates.ts, 33, 26)) + + return x !== null; +>x : Symbol(x, Decl(inferTypePredicates.ts, 33, 29)) +} + +// Type guards can flow between functions +const myGuard = (o: string | undefined): o is string => !!o; +>myGuard : Symbol(myGuard, Decl(inferTypePredicates.ts, 38, 5)) +>o : Symbol(o, Decl(inferTypePredicates.ts, 38, 17)) +>o : Symbol(o, Decl(inferTypePredicates.ts, 38, 17)) +>o : Symbol(o, Decl(inferTypePredicates.ts, 38, 17)) + +const mySecondGuard = (o: string | undefined) => myGuard(o); +>mySecondGuard : Symbol(mySecondGuard, Decl(inferTypePredicates.ts, 39, 5)) +>o : Symbol(o, Decl(inferTypePredicates.ts, 39, 23)) +>myGuard : Symbol(myGuard, Decl(inferTypePredicates.ts, 38, 5)) +>o : Symbol(o, Decl(inferTypePredicates.ts, 39, 23)) + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1327449914 +// This doesn't work because the false condition prevents type guard inference. +// Breaking up the filters does work. +type MyObj = { data?: string }; +>MyObj : Symbol(MyObj, Decl(inferTypePredicates.ts, 39, 60)) +>data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) + +type MyArray = { list?: MyObj[] }[]; +>MyArray : Symbol(MyArray, Decl(inferTypePredicates.ts, 44, 31)) +>list : Symbol(list, Decl(inferTypePredicates.ts, 45, 16)) +>MyObj : Symbol(MyObj, Decl(inferTypePredicates.ts, 39, 60)) + +const myArray: MyArray = []; +>myArray : Symbol(myArray, Decl(inferTypePredicates.ts, 46, 5)) +>MyArray : Symbol(MyArray, Decl(inferTypePredicates.ts, 44, 31)) + +const result = myArray +>result : Symbol(result, Decl(inferTypePredicates.ts, 48, 5)) +>myArray .map((arr) => arr.list) .filter((arr) => arr && arr.length) .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>myArray .map((arr) => arr.list) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>myArray .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>myArray : Symbol(myArray, Decl(inferTypePredicates.ts, 46, 5)) + + .map((arr) => arr.list) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 49, 8)) +>arr.list : Symbol(list, Decl(inferTypePredicates.ts, 45, 16)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 49, 8)) +>list : Symbol(list, Decl(inferTypePredicates.ts, 45, 16)) + + .filter((arr) => arr && arr.length) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 50, 11)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 50, 11)) +>arr.length : Symbol(Array.length, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 50, 11)) +>length : Symbol(Array.length, Decl(lib.es5.d.ts, --, --)) + + .map((arr) => arr // should error +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 51, 8)) +>arr // should error .filter((obj) => obj && obj.data) .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr // should error .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 51, 8)) + + .filter((obj) => obj && obj.data) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 52, 13)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 52, 13)) +>obj.data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 52, 13)) +>data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) + + .map(obj => JSON.parse(obj.data)) // should error +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 53, 9)) +>JSON.parse : Symbol(JSON.parse, Decl(lib.es5.d.ts, --, --)) +>JSON : Symbol(JSON, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>parse : Symbol(JSON.parse, Decl(lib.es5.d.ts, --, --)) +>obj.data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 53, 9)) +>data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) + + ); + +const result2 = myArray +>result2 : Symbol(result2, Decl(inferTypePredicates.ts, 56, 5)) +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter(arr => arr.length) .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>myArray .map((arr) => arr.list) .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>myArray .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>myArray : Symbol(myArray, Decl(inferTypePredicates.ts, 46, 5)) + + .map((arr) => arr.list) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 57, 8)) +>arr.list : Symbol(list, Decl(inferTypePredicates.ts, 45, 16)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 57, 8)) +>list : Symbol(list, Decl(inferTypePredicates.ts, 45, 16)) + + .filter((arr) => !!arr) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 58, 11)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 58, 11)) + + .filter(arr => arr.length) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 59, 10)) +>arr.length : Symbol(Array.length, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 59, 10)) +>length : Symbol(Array.length, Decl(lib.es5.d.ts, --, --)) + + .map((arr) => arr // should ok +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 60, 8)) +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) .map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr // should ok .filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferTypePredicates.ts, 60, 8)) + + .filter((obj) => obj) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 61, 13)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 61, 13)) + + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(obj => !!obj.data) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 63, 12)) +>obj.data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 63, 12)) +>data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) + + .map(obj => JSON.parse(obj.data)) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 64, 9)) +>JSON.parse : Symbol(JSON.parse, Decl(lib.es5.d.ts, --, --)) +>JSON : Symbol(JSON, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>parse : Symbol(JSON.parse, Decl(lib.es5.d.ts, --, --)) +>obj.data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) +>obj : Symbol(obj, Decl(inferTypePredicates.ts, 64, 9)) +>data : Symbol(data, Decl(inferTypePredicates.ts, 44, 14)) + + ); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1183547889 +type Foo = { +>Foo : Symbol(Foo, Decl(inferTypePredicates.ts, 65, 4)) + + foo: string; +>foo : Symbol(foo, Decl(inferTypePredicates.ts, 68, 12)) +} +type Bar = Foo & { +>Bar : Symbol(Bar, Decl(inferTypePredicates.ts, 70, 1)) +>Foo : Symbol(Foo, Decl(inferTypePredicates.ts, 65, 4)) + + bar: string; +>bar : Symbol(bar, Decl(inferTypePredicates.ts, 71, 18)) +} + +const list: (Foo | Bar)[] = []; +>list : Symbol(list, Decl(inferTypePredicates.ts, 75, 5)) +>Foo : Symbol(Foo, Decl(inferTypePredicates.ts, 65, 4)) +>Bar : Symbol(Bar, Decl(inferTypePredicates.ts, 70, 1)) + +const resultBars: Bar[] = list.filter((value) => 'bar' in value); // should ok +>resultBars : Symbol(resultBars, Decl(inferTypePredicates.ts, 76, 5)) +>Bar : Symbol(Bar, Decl(inferTypePredicates.ts, 70, 1)) +>list.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>list : Symbol(list, Decl(inferTypePredicates.ts, 75, 5)) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>value : Symbol(value, Decl(inferTypePredicates.ts, 76, 39)) +>value : Symbol(value, Decl(inferTypePredicates.ts, 76, 39)) + +function isBarNonNull(x: Foo | Bar | null) { +>isBarNonNull : Symbol(isBarNonNull, Decl(inferTypePredicates.ts, 76, 65)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 78, 22)) +>Foo : Symbol(Foo, Decl(inferTypePredicates.ts, 65, 4)) +>Bar : Symbol(Bar, Decl(inferTypePredicates.ts, 70, 1)) + + return ('bar' in x!); +>x : Symbol(x, Decl(inferTypePredicates.ts, 78, 22)) +} +const fooOrBar = list[0]; +>fooOrBar : Symbol(fooOrBar, Decl(inferTypePredicates.ts, 81, 5)) +>list : Symbol(list, Decl(inferTypePredicates.ts, 75, 5)) + +if (isBarNonNull(fooOrBar)) { +>isBarNonNull : Symbol(isBarNonNull, Decl(inferTypePredicates.ts, 76, 65)) +>fooOrBar : Symbol(fooOrBar, Decl(inferTypePredicates.ts, 81, 5)) + + const t: Bar = fooOrBar; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 83, 7)) +>Bar : Symbol(Bar, Decl(inferTypePredicates.ts, 70, 1)) +>fooOrBar : Symbol(fooOrBar, Decl(inferTypePredicates.ts, 81, 5)) +} + +// https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 +// Ryan's example (currently legal): +const a = [1, "foo", 2, "bar"].filter(x => typeof x === "string"); +>a : Symbol(a, Decl(inferTypePredicates.ts, 88, 5)) +>[1, "foo", 2, "bar"].filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 88, 38)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 88, 38)) + +a.push(10); +>a.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>a : Symbol(a, Decl(inferTypePredicates.ts, 88, 5)) +>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) + +// Defer to explicit type guards, even when they're incorrect. +function backwardsGuard(x: number|string): x is number { +>backwardsGuard : Symbol(backwardsGuard, Decl(inferTypePredicates.ts, 89, 11)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 92, 24)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 92, 24)) + + return typeof x === 'string'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 92, 24)) +} + +// Partition tests. The "false" case matters. +function isString(x: string | number) { +>isString : Symbol(isString, Decl(inferTypePredicates.ts, 94, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 97, 18)) + + return typeof x === 'string'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 97, 18)) +} + +declare let strOrNum: string | number; +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) + +if (isString(strOrNum)) { +>isString : Symbol(isString, Decl(inferTypePredicates.ts, 94, 1)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) + + let t: string = strOrNum; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 103, 5)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) + +} else { + let t: number = strOrNum; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 105, 5)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) +} + +function flakyIsString(x: string | number) { +>flakyIsString : Symbol(flakyIsString, Decl(inferTypePredicates.ts, 106, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 108, 23)) + + return typeof x === 'string' && Math.random() > 0.5; +>x : Symbol(x, Decl(inferTypePredicates.ts, 108, 23)) +>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +} +if (flakyIsString(strOrNum)) { +>flakyIsString : Symbol(flakyIsString, Decl(inferTypePredicates.ts, 106, 1)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) + + let t: string = strOrNum; // should error +>t : Symbol(t, Decl(inferTypePredicates.ts, 112, 5)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) + +} else { + let t: number = strOrNum; // should error +>t : Symbol(t, Decl(inferTypePredicates.ts, 114, 5)) +>strOrNum : Symbol(strOrNum, Decl(inferTypePredicates.ts, 101, 11)) +} + +function isDate(x: object): x is Date { +>isDate : Symbol(isDate, Decl(inferTypePredicates.ts, 115, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 117, 16)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 117, 16)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) + + return x instanceof Date; +>x : Symbol(x, Decl(inferTypePredicates.ts, 117, 16)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +} +function flakyIsDate(x: object): x is Date { +>flakyIsDate : Symbol(flakyIsDate, Decl(inferTypePredicates.ts, 119, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 120, 21)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 120, 21)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) + + return x instanceof Date; +>x : Symbol(x, Decl(inferTypePredicates.ts, 120, 21)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +} + +declare let maybeDate: object; +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) + +if (isDate(maybeDate)) { +>isDate : Symbol(isDate, Decl(inferTypePredicates.ts, 115, 1)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) + + let t: Date = maybeDate; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 126, 5)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) + +} else { + let t: object = maybeDate; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 128, 5)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) +} + +if (flakyIsDate(maybeDate)) { +>flakyIsDate : Symbol(flakyIsDate, Decl(inferTypePredicates.ts, 119, 1)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) + + let t: Date = maybeDate; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 132, 5)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) + +} else { + let t: object = maybeDate; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 134, 5)) +>maybeDate : Symbol(maybeDate, Decl(inferTypePredicates.ts, 124, 11)) +} + +// This should not infer a type guard since the value on which we do the refinement +// is not related to the original parameter. +function irrelevantIsNumber(x: string | number) { +>irrelevantIsNumber : Symbol(irrelevantIsNumber, Decl(inferTypePredicates.ts, 135, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 139, 28)) + + x = Math.random() < 0.5 ? "string" : 123; +>x : Symbol(x, Decl(inferTypePredicates.ts, 139, 28)) +>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) + + return typeof x === 'string'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 139, 28)) +} +function irrelevantIsNumberDestructuring(x: string | number) { +>irrelevantIsNumberDestructuring : Symbol(irrelevantIsNumberDestructuring, Decl(inferTypePredicates.ts, 142, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 143, 41)) + + [x] = [Math.random() < 0.5 ? "string" : 123]; +>x : Symbol(x, Decl(inferTypePredicates.ts, 143, 41)) +>Math.random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>random : Symbol(Math.random, Decl(lib.es5.d.ts, --, --)) + + return typeof x === 'string'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 143, 41)) +} + +// Cannot infer a type guard for either param because of the false case. +function areBothNums(x: string|number, y: string|number) { +>areBothNums : Symbol(areBothNums, Decl(inferTypePredicates.ts, 146, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 149, 21)) +>y : Symbol(y, Decl(inferTypePredicates.ts, 149, 38)) + + return typeof x === 'number' && typeof y === 'number'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 149, 21)) +>y : Symbol(y, Decl(inferTypePredicates.ts, 149, 38)) +} + +// Could potentially infer a type guard here but it would require more bookkeeping. +function doubleReturn(x: string|number) { +>doubleReturn : Symbol(doubleReturn, Decl(inferTypePredicates.ts, 151, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 154, 22)) + + if (typeof x === 'string') { +>x : Symbol(x, Decl(inferTypePredicates.ts, 154, 22)) + + return true; + } + return false; +} + +function guardsOneButNotOthers(a: string|number, b: string|number, c: string|number) { +>guardsOneButNotOthers : Symbol(guardsOneButNotOthers, Decl(inferTypePredicates.ts, 159, 1)) +>a : Symbol(a, Decl(inferTypePredicates.ts, 161, 31)) +>b : Symbol(b, Decl(inferTypePredicates.ts, 161, 48)) +>c : Symbol(c, Decl(inferTypePredicates.ts, 161, 66)) + + return typeof b === 'string'; +>b : Symbol(b, Decl(inferTypePredicates.ts, 161, 48)) +} + +// String escaping issue (please help!) +function dunderguard(__x: number | string) { +>dunderguard : Symbol(dunderguard, Decl(inferTypePredicates.ts, 163, 1)) +>__x : Symbol(__x, Decl(inferTypePredicates.ts, 166, 21)) + + return typeof __x === 'string'; +>__x : Symbol(__x, Decl(inferTypePredicates.ts, 166, 21)) +} + +// could infer a type guard here but it doesn't seem that helpful. +const booleanIdentity = (x: boolean) => x; +>booleanIdentity : Symbol(booleanIdentity, Decl(inferTypePredicates.ts, 171, 5)) +>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; +>numOrBoolean : Symbol(numOrBoolean, Decl(inferTypePredicates.ts, 174, 5)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 174, 22)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 174, 22)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 174, 22)) + +// inferred guards in methods +interface NumberInferrer { +>NumberInferrer : Symbol(NumberInferrer, Decl(inferTypePredicates.ts, 174, 73)) + + isNumber(x: number | string): x is number; +>isNumber : Symbol(NumberInferrer.isNumber, Decl(inferTypePredicates.ts, 177, 26)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 178, 11)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 178, 11)) +} +class Inferrer implements NumberInferrer { +>Inferrer : Symbol(Inferrer, Decl(inferTypePredicates.ts, 179, 1)) +>NumberInferrer : Symbol(NumberInferrer, Decl(inferTypePredicates.ts, 174, 73)) + + isNumber(x: number | string) { // should ok +>isNumber : Symbol(Inferrer.isNumber, Decl(inferTypePredicates.ts, 180, 42)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 181, 11)) + + return typeof x === 'number'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 181, 11)) + } +} +declare let numOrStr: number | string; +>numOrStr : Symbol(numOrStr, Decl(inferTypePredicates.ts, 185, 11)) + +const inf = new Inferrer(); +>inf : Symbol(inf, Decl(inferTypePredicates.ts, 186, 5)) +>Inferrer : Symbol(Inferrer, Decl(inferTypePredicates.ts, 179, 1)) + +if (inf.isNumber(numOrStr)) { +>inf.isNumber : Symbol(Inferrer.isNumber, Decl(inferTypePredicates.ts, 180, 42)) +>inf : Symbol(inf, Decl(inferTypePredicates.ts, 186, 5)) +>isNumber : Symbol(Inferrer.isNumber, Decl(inferTypePredicates.ts, 180, 42)) +>numOrStr : Symbol(numOrStr, Decl(inferTypePredicates.ts, 185, 11)) + + let t: number = numOrStr; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 188, 5)) +>numOrStr : Symbol(numOrStr, Decl(inferTypePredicates.ts, 185, 11)) + +} else { + let t: string = numOrStr; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 190, 5)) +>numOrStr : Symbol(numOrStr, Decl(inferTypePredicates.ts, 185, 11)) +} + +// Type predicates are not inferred on "this" +class C1 { +>C1 : Symbol(C1, Decl(inferTypePredicates.ts, 191, 1)) + + isC2() { +>isC2 : Symbol(C1.isC2, Decl(inferTypePredicates.ts, 194, 10)) + + return this instanceof C2; +>this : Symbol(C1, Decl(inferTypePredicates.ts, 191, 1)) +>C2 : Symbol(C2, Decl(inferTypePredicates.ts, 198, 1)) + } +} +class C2 extends C1 { +>C2 : Symbol(C2, Decl(inferTypePredicates.ts, 198, 1)) +>C1 : Symbol(C1, Decl(inferTypePredicates.ts, 191, 1)) + + z = 0; +>z : Symbol(C2.z, Decl(inferTypePredicates.ts, 199, 21)) +} +declare let c: C1; +>c : Symbol(c, Decl(inferTypePredicates.ts, 202, 11)) +>C1 : Symbol(C1, Decl(inferTypePredicates.ts, 191, 1)) + +if (c.isC2()) { +>c.isC2 : Symbol(C1.isC2, Decl(inferTypePredicates.ts, 194, 10)) +>c : Symbol(c, Decl(inferTypePredicates.ts, 202, 11)) +>isC2 : Symbol(C1.isC2, Decl(inferTypePredicates.ts, 194, 10)) + + let c2: C2 = c; // should error +>c2 : Symbol(c2, Decl(inferTypePredicates.ts, 204, 5)) +>C2 : Symbol(C2, Decl(inferTypePredicates.ts, 198, 1)) +>c : Symbol(c, Decl(inferTypePredicates.ts, 202, 11)) +} + +function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { +>doNotRefineDestructuredParam : Symbol(doNotRefineDestructuredParam, Decl(inferTypePredicates.ts, 205, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 207, 39)) +>y : Symbol(y, Decl(inferTypePredicates.ts, 207, 41)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 207, 47)) +>y : Symbol(y, Decl(inferTypePredicates.ts, 207, 64)) + + return typeof x === 'number'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 207, 39)) +} + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types new file mode 100644 index 0000000000000..8764f712cbac9 --- /dev/null +++ b/tests/baselines/reference/inferTypePredicates.types @@ -0,0 +1,786 @@ +//// [tests/cases/compiler/inferTypePredicates.ts] //// + +=== inferTypePredicates.ts === +// https://github.com/microsoft/TypeScript/issues/16069 + +const numsOrNull = [1, 2, 3, 4, null]; +>numsOrNull : (number | null)[] +>[1, 2, 3, 4, null] : (number | null)[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 + +const filteredNumsTruthy: number[] = numsOrNull.filter(x => !!x); // should error +>filteredNumsTruthy : number[] +>numsOrNull.filter(x => !!x) : (number | null)[] +>numsOrNull.filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>numsOrNull : (number | null)[] +>filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>x => !!x : (x: number | null) => boolean +>x : number | null +>!!x : boolean +>!x : boolean +>x : number | null + +const filteredNumsNonNullish: number[] = numsOrNull.filter(x => x !== null); // should ok +>filteredNumsNonNullish : number[] +>numsOrNull.filter(x => x !== null) : number[] +>numsOrNull.filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>numsOrNull : (number | null)[] +>filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>x => x !== null : (x: number | null) => x is number +>x : number | null +>x !== null : boolean +>x : number | null + +const evenSquaresInline: number[] = // should error +>evenSquaresInline : number[] + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter(x => !!x) : (number | null)[] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) : (number | null)[] +>[1, 2, 3, 4] .map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>[1, 2, 3, 4] : number[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 + + .map(x => x % 2 === 0 ? x * x : null) +>map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>x => x % 2 === 0 ? x * x : null : (x: number) => number | null +>x : number +>x % 2 === 0 ? x * x : null : number | null +>x % 2 === 0 : boolean +>x % 2 : number +>x : number +>2 : 2 +>0 : 0 +>x * x : number +>x : number +>x : number + + .filter(x => !!x); // tests truthiness, not non-nullishness +>filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>x => !!x : (x: number | null) => boolean +>x : number | null +>!!x : boolean +>!x : boolean +>x : number | null + +const isTruthy = (x: number | null) => !!x; +>isTruthy : (x: number | null) => boolean +>(x: number | null) => !!x : (x: number | null) => boolean +>x : number | null +>!!x : boolean +>!x : boolean +>x : number | null + +const evenSquares: number[] = // should error +>evenSquares : number[] + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter(isTruthy) : (number | null)[] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) : (number | null)[] +>[1, 2, 3, 4] .map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>[1, 2, 3, 4] : number[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 + + .map(x => x % 2 === 0 ? x * x : null) +>map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>x => x % 2 === 0 ? x * x : null : (x: number) => number | null +>x : number +>x % 2 === 0 ? x * x : null : number | null +>x % 2 === 0 : boolean +>x % 2 : number +>x : number +>2 : 2 +>0 : 0 +>x * x : number +>x : number +>x : number + + .filter(isTruthy); +>filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>isTruthy : (x: number | null) => boolean + +const evenSquaresNonNull: number[] = // should ok +>evenSquaresNonNull : number[] + + [1, 2, 3, 4] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter(x => x !== null) : number[] +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) .filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>[1, 2, 3, 4] .map(x => x % 2 === 0 ? x * x : null) : (number | null)[] +>[1, 2, 3, 4] .map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>[1, 2, 3, 4] : number[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 + + .map(x => x % 2 === 0 ? x * x : null) +>map : (callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[] +>x => x % 2 === 0 ? x * x : null : (x: number) => number | null +>x : number +>x % 2 === 0 ? x * x : null : number | null +>x % 2 === 0 : boolean +>x % 2 : number +>x : number +>2 : 2 +>0 : 0 +>x * x : number +>x : number +>x : number + + .filter(x => x !== null); +>filter : { (predicate: (value: number | null, index: number, array: (number | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: number | null, index: number, array: (number | null)[]) => unknown, thisArg?: any): (number | null)[]; } +>x => x !== null : (x: number | null) => x is number +>x : number | null +>x !== null : boolean +>x : number | null + +function isNonNull(x: number | null) { +>isNonNull : (x: number | null) => x is number +>x : number | null + + return x !== null; +>x !== null : boolean +>x : number | null +} + +// factoring out a boolean works thanks to aliased discriminants +function isNonNullVar(x: number | null) { +>isNonNullVar : (x: number | null) => x is number +>x : number | null + + const ok = x !== null; +>ok : boolean +>x !== null : boolean +>x : number | null + + return ok; +>ok : boolean +} + +function isNonNullGeneric(x: T) { +>isNonNullGeneric : (x: T) => x is T & ({} | undefined) +>x : T + + return x !== null; +>x !== null : boolean +>x : T +} + +// Type guards can flow between functions +const myGuard = (o: string | undefined): o is string => !!o; +>myGuard : (o: string | undefined) => o is string +>(o: string | undefined): o is string => !!o : (o: string | undefined) => o is string +>o : string | undefined +>!!o : boolean +>!o : boolean +>o : string | undefined + +const mySecondGuard = (o: string | undefined) => myGuard(o); +>mySecondGuard : (o: string | undefined) => o is string +>(o: string | undefined) => myGuard(o) : (o: string | undefined) => o is string +>o : string | undefined +>myGuard(o) : boolean +>myGuard : (o: string | undefined) => o is string +>o : string | undefined + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1327449914 +// This doesn't work because the false condition prevents type guard inference. +// Breaking up the filters does work. +type MyObj = { data?: string }; +>MyObj : { data?: string | undefined; } +>data : string | undefined + +type MyArray = { list?: MyObj[] }[]; +>MyArray : { list?: MyObj[] | undefined; }[] +>list : MyObj[] | undefined + +const myArray: MyArray = []; +>myArray : MyArray +>[] : never[] + +const result = myArray +>result : any[][] +>myArray .map((arr) => arr.list) .filter((arr) => arr && arr.length) .map((arr) => arr // should error .filter((obj) => obj && obj.data) .map(obj => JSON.parse(obj.data)) // should error ) : any[][] +>myArray .map((arr) => arr.list) .filter((arr) => arr && arr.length) .map : (callbackfn: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => U, thisArg?: any) => U[] +>myArray .map((arr) => arr.list) .filter((arr) => arr && arr.length) : (MyObj[] | undefined)[] +>myArray .map((arr) => arr.list) .filter : { (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => unknown, thisArg?: any): (MyObj[] | undefined)[]; } +>myArray .map((arr) => arr.list) : (MyObj[] | undefined)[] +>myArray .map : (callbackfn: (value: { list?: MyObj[] | undefined; }, index: number, array: { list?: MyObj[] | undefined; }[]) => U, thisArg?: any) => U[] +>myArray : MyArray + + .map((arr) => arr.list) +>map : (callbackfn: (value: { list?: MyObj[] | undefined; }, index: number, array: { list?: MyObj[] | undefined; }[]) => U, thisArg?: any) => U[] +>(arr) => arr.list : (arr: { list?: MyObj[] | undefined; }) => MyObj[] | undefined +>arr : { list?: MyObj[] | undefined; } +>arr.list : MyObj[] | undefined +>arr : { list?: MyObj[] | undefined; } +>list : MyObj[] | undefined + + .filter((arr) => arr && arr.length) +>filter : { (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => unknown, thisArg?: any): (MyObj[] | undefined)[]; } +>(arr) => arr && arr.length : (arr: MyObj[] | undefined) => number | undefined +>arr : MyObj[] | undefined +>arr && arr.length : number | undefined +>arr : MyObj[] | undefined +>arr.length : number +>arr : MyObj[] +>length : number + + .map((arr) => arr // should error +>map : (callbackfn: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => U, thisArg?: any) => U[] +>(arr) => arr // should error .filter((obj) => obj && obj.data) .map(obj => JSON.parse(obj.data)) : (arr: MyObj[] | undefined) => any[] +>arr : MyObj[] | undefined +>arr // should error .filter((obj) => obj && obj.data) .map(obj => JSON.parse(obj.data)) : any[] +>arr // should error .filter((obj) => obj && obj.data) .map : (callbackfn: (value: MyObj, index: number, array: MyObj[]) => U, thisArg?: any) => U[] +>arr // should error .filter((obj) => obj && obj.data) : MyObj[] +>arr // should error .filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>arr : MyObj[] | undefined + + .filter((obj) => obj && obj.data) +>filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>(obj) => obj && obj.data : (obj: MyObj) => string | undefined +>obj : MyObj +>obj && obj.data : string | undefined +>obj : MyObj +>obj.data : string | undefined +>obj : MyObj +>data : string | undefined + + .map(obj => JSON.parse(obj.data)) // should error +>map : (callbackfn: (value: MyObj, index: number, array: MyObj[]) => U, thisArg?: any) => U[] +>obj => JSON.parse(obj.data) : (obj: MyObj) => any +>obj : MyObj +>JSON.parse(obj.data) : any +>JSON.parse : (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any +>JSON : JSON +>parse : (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any +>obj.data : string | undefined +>obj : MyObj +>data : string | undefined + + ); + +const result2 = myArray +>result2 : any[][] +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter(arr => arr.length) .map((arr) => arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) .map(obj => JSON.parse(obj.data)) ) : any[][] +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter(arr => arr.length) .map : (callbackfn: (value: MyObj[], index: number, array: MyObj[][]) => U, thisArg?: any) => U[] +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter(arr => arr.length) : MyObj[][] +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) .filter : { (predicate: (value: MyObj[], index: number, array: MyObj[][]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[], index: number, array: MyObj[][]) => unknown, thisArg?: any): MyObj[][]; } +>myArray .map((arr) => arr.list) .filter((arr) => !!arr) : MyObj[][] +>myArray .map((arr) => arr.list) .filter : { (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => unknown, thisArg?: any): (MyObj[] | undefined)[]; } +>myArray .map((arr) => arr.list) : (MyObj[] | undefined)[] +>myArray .map : (callbackfn: (value: { list?: MyObj[] | undefined; }, index: number, array: { list?: MyObj[] | undefined; }[]) => U, thisArg?: any) => U[] +>myArray : MyArray + + .map((arr) => arr.list) +>map : (callbackfn: (value: { list?: MyObj[] | undefined; }, index: number, array: { list?: MyObj[] | undefined; }[]) => U, thisArg?: any) => U[] +>(arr) => arr.list : (arr: { list?: MyObj[] | undefined; }) => MyObj[] | undefined +>arr : { list?: MyObj[] | undefined; } +>arr.list : MyObj[] | undefined +>arr : { list?: MyObj[] | undefined; } +>list : MyObj[] | undefined + + .filter((arr) => !!arr) +>filter : { (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[] | undefined, index: number, array: (MyObj[] | undefined)[]) => unknown, thisArg?: any): (MyObj[] | undefined)[]; } +>(arr) => !!arr : (arr: MyObj[] | undefined) => arr is MyObj[] +>arr : MyObj[] | undefined +>!!arr : boolean +>!arr : boolean +>arr : MyObj[] | undefined + + .filter(arr => arr.length) +>filter : { (predicate: (value: MyObj[], index: number, array: MyObj[][]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj[], index: number, array: MyObj[][]) => unknown, thisArg?: any): MyObj[][]; } +>arr => arr.length : (arr: MyObj[]) => number +>arr : MyObj[] +>arr.length : number +>arr : MyObj[] +>length : number + + .map((arr) => arr // should ok +>map : (callbackfn: (value: MyObj[], index: number, array: MyObj[][]) => U, thisArg?: any) => U[] +>(arr) => arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) .map(obj => JSON.parse(obj.data)) : (arr: MyObj[]) => any[] +>arr : MyObj[] +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) .map(obj => JSON.parse(obj.data)) : any[] +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) .map : (callbackfn: (value: MyObj, index: number, array: MyObj[]) => U, thisArg?: any) => U[] +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter(obj => !!obj.data) : MyObj[] +>arr // should ok .filter((obj) => obj) // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 .filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>arr // should ok .filter((obj) => obj) : MyObj[] +>arr // should ok .filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>arr : MyObj[] + + .filter((obj) => obj) +>filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>(obj) => obj : (obj: MyObj) => MyObj +>obj : MyObj +>obj : MyObj + + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(obj => !!obj.data) +>filter : { (predicate: (value: MyObj, index: number, array: MyObj[]) => value is S, thisArg?: any): S[]; (predicate: (value: MyObj, index: number, array: MyObj[]) => unknown, thisArg?: any): MyObj[]; } +>obj => !!obj.data : (obj: MyObj) => boolean +>obj : MyObj +>!!obj.data : boolean +>!obj.data : boolean +>obj.data : string | undefined +>obj : MyObj +>data : string | undefined + + .map(obj => JSON.parse(obj.data)) +>map : (callbackfn: (value: MyObj, index: number, array: MyObj[]) => U, thisArg?: any) => U[] +>obj => JSON.parse(obj.data) : (obj: MyObj) => any +>obj : MyObj +>JSON.parse(obj.data) : any +>JSON.parse : (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any +>JSON : JSON +>parse : (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any +>obj.data : string | undefined +>obj : MyObj +>data : string | undefined + + ); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1183547889 +type Foo = { +>Foo : { foo: string; } + + foo: string; +>foo : string +} +type Bar = Foo & { +>Bar : Foo & { bar: string; } + + bar: string; +>bar : string +} + +const list: (Foo | Bar)[] = []; +>list : (Foo | Bar)[] +>[] : never[] + +const resultBars: Bar[] = list.filter((value) => 'bar' in value); // should ok +>resultBars : Bar[] +>list.filter((value) => 'bar' in value) : Bar[] +>list.filter : { (predicate: (value: Foo | Bar, index: number, array: (Foo | Bar)[]) => value is S, thisArg?: any): S[]; (predicate: (value: Foo | Bar, index: number, array: (Foo | Bar)[]) => unknown, thisArg?: any): (Foo | Bar)[]; } +>list : (Foo | Bar)[] +>filter : { (predicate: (value: Foo | Bar, index: number, array: (Foo | Bar)[]) => value is S, thisArg?: any): S[]; (predicate: (value: Foo | Bar, index: number, array: (Foo | Bar)[]) => unknown, thisArg?: any): (Foo | Bar)[]; } +>(value) => 'bar' in value : (value: Foo | Bar) => value is Bar +>value : Foo | Bar +>'bar' in value : boolean +>'bar' : "bar" +>value : Foo | Bar + +function isBarNonNull(x: Foo | Bar | null) { +>isBarNonNull : (x: Foo | Bar | null) => x is Bar +>x : Foo | Bar | null + + return ('bar' in x!); +>('bar' in x!) : boolean +>'bar' in x! : boolean +>'bar' : "bar" +>x! : Foo | Bar +>x : Foo | Bar | null +} +const fooOrBar = list[0]; +>fooOrBar : Foo | Bar +>list[0] : Foo | Bar +>list : (Foo | Bar)[] +>0 : 0 + +if (isBarNonNull(fooOrBar)) { +>isBarNonNull(fooOrBar) : boolean +>isBarNonNull : (x: Foo | Bar | null) => x is Bar +>fooOrBar : Foo | Bar + + const t: Bar = fooOrBar; // should ok +>t : Bar +>fooOrBar : Bar +} + +// https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 +// Ryan's example (currently legal): +const a = [1, "foo", 2, "bar"].filter(x => typeof x === "string"); +>a : string[] +>[1, "foo", 2, "bar"].filter(x => typeof x === "string") : string[] +>[1, "foo", 2, "bar"].filter : { (predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; } +>[1, "foo", 2, "bar"] : (string | number)[] +>1 : 1 +>"foo" : "foo" +>2 : 2 +>"bar" : "bar" +>filter : { (predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; } +>x => typeof x === "string" : (x: string | number) => x is string +>x : string | number +>typeof x === "string" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>"string" : "string" + +a.push(10); +>a.push(10) : number +>a.push : (...items: string[]) => number +>a : string[] +>push : (...items: string[]) => number +>10 : 10 + +// Defer to explicit type guards, even when they're incorrect. +function backwardsGuard(x: number|string): x is number { +>backwardsGuard : (x: number | string) => x is number +>x : string | number + + return typeof x === 'string'; +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +} + +// Partition tests. The "false" case matters. +function isString(x: string | number) { +>isString : (x: string | number) => x is string +>x : string | number + + return typeof x === 'string'; +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +} + +declare let strOrNum: string | number; +>strOrNum : string | number + +if (isString(strOrNum)) { +>isString(strOrNum) : boolean +>isString : (x: string | number) => x is string +>strOrNum : string | number + + let t: string = strOrNum; // should ok +>t : string +>strOrNum : string + +} else { + let t: number = strOrNum; // should ok +>t : number +>strOrNum : number +} + +function flakyIsString(x: string | number) { +>flakyIsString : (x: string | number) => boolean +>x : string | number + + return typeof x === 'string' && Math.random() > 0.5; +>typeof x === 'string' && Math.random() > 0.5 : boolean +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +>Math.random() > 0.5 : boolean +>Math.random() : number +>Math.random : () => number +>Math : Math +>random : () => number +>0.5 : 0.5 +} +if (flakyIsString(strOrNum)) { +>flakyIsString(strOrNum) : boolean +>flakyIsString : (x: string | number) => boolean +>strOrNum : string | number + + let t: string = strOrNum; // should error +>t : string +>strOrNum : string | number + +} else { + let t: number = strOrNum; // should error +>t : number +>strOrNum : string | number +} + +function isDate(x: object): x is Date { +>isDate : (x: object) => x is Date +>x : object + + return x instanceof Date; +>x instanceof Date : boolean +>x : object +>Date : DateConstructor +} +function flakyIsDate(x: object): x is Date { +>flakyIsDate : (x: object) => x is Date +>x : object + + return x instanceof Date; +>x instanceof Date : boolean +>x : object +>Date : DateConstructor +} + +declare let maybeDate: object; +>maybeDate : object + +if (isDate(maybeDate)) { +>isDate(maybeDate) : boolean +>isDate : (x: object) => x is Date +>maybeDate : object + + let t: Date = maybeDate; // should ok +>t : Date +>maybeDate : Date + +} else { + let t: object = maybeDate; // should ok +>t : object +>maybeDate : object +} + +if (flakyIsDate(maybeDate)) { +>flakyIsDate(maybeDate) : boolean +>flakyIsDate : (x: object) => x is Date +>maybeDate : object + + let t: Date = maybeDate; // should ok +>t : Date +>maybeDate : Date + +} else { + let t: object = maybeDate; // should ok +>t : object +>maybeDate : object +} + +// This should not infer a type guard since the value on which we do the refinement +// is not related to the original parameter. +function irrelevantIsNumber(x: string | number) { +>irrelevantIsNumber : (x: string | number) => boolean +>x : string | number + + x = Math.random() < 0.5 ? "string" : 123; +>x = Math.random() < 0.5 ? "string" : 123 : "string" | 123 +>x : string | number +>Math.random() < 0.5 ? "string" : 123 : "string" | 123 +>Math.random() < 0.5 : boolean +>Math.random() : number +>Math.random : () => number +>Math : Math +>random : () => number +>0.5 : 0.5 +>"string" : "string" +>123 : 123 + + return typeof x === 'string'; +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +} +function irrelevantIsNumberDestructuring(x: string | number) { +>irrelevantIsNumberDestructuring : (x: string | number) => boolean +>x : string | number + + [x] = [Math.random() < 0.5 ? "string" : 123]; +>[x] = [Math.random() < 0.5 ? "string" : 123] : [string | number] +>[x] : [string | number] +>x : string | number +>[Math.random() < 0.5 ? "string" : 123] : [string | number] +>Math.random() < 0.5 ? "string" : 123 : "string" | 123 +>Math.random() < 0.5 : boolean +>Math.random() : number +>Math.random : () => number +>Math : Math +>random : () => number +>0.5 : 0.5 +>"string" : "string" +>123 : 123 + + return typeof x === 'string'; +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +} + +// Cannot infer a type guard for either param because of the false case. +function areBothNums(x: string|number, y: string|number) { +>areBothNums : (x: string | number, y: string | number) => boolean +>x : string | number +>y : string | number + + return typeof x === 'number' && typeof y === 'number'; +>typeof x === 'number' && typeof y === 'number' : boolean +>typeof x === 'number' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'number' : "number" +>typeof y === 'number' : boolean +>typeof y : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>y : string | number +>'number' : "number" +} + +// Could potentially infer a type guard here but it would require more bookkeeping. +function doubleReturn(x: string|number) { +>doubleReturn : (x: string | number) => boolean +>x : string | number + + if (typeof x === 'string') { +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" + + return true; +>true : true + } + return false; +>false : false +} + +function guardsOneButNotOthers(a: string|number, b: string|number, c: string|number) { +>guardsOneButNotOthers : (a: string | number, b: string | number, c: string | number) => b is string +>a : string | number +>b : string | number +>c : string | number + + return typeof b === 'string'; +>typeof b === 'string' : boolean +>typeof b : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>b : string | number +>'string' : "string" +} + +// String escaping issue (please help!) +function dunderguard(__x: number | string) { +>dunderguard : (__x: number | string) => ___x is string +>__x : string | number + + return typeof __x === 'string'; +>typeof __x === 'string' : boolean +>typeof __x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>__x : string | number +>'string' : "string" +} + +// could infer a type guard here but it doesn't seem that helpful. +const booleanIdentity = (x: boolean) => x; +>booleanIdentity : (x: boolean) => boolean +>(x: boolean) => x : (x: boolean) => boolean +>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 +>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" +>x : boolean + +// inferred guards in methods +interface NumberInferrer { + isNumber(x: number | string): x is number; +>isNumber : (x: number | string) => x is number +>x : string | number +} +class Inferrer implements NumberInferrer { +>Inferrer : Inferrer + + isNumber(x: number | string) { // should ok +>isNumber : (x: number | string) => x is number +>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" + } +} +declare let numOrStr: number | string; +>numOrStr : string | number + +const inf = new Inferrer(); +>inf : Inferrer +>new Inferrer() : Inferrer +>Inferrer : typeof Inferrer + +if (inf.isNumber(numOrStr)) { +>inf.isNumber(numOrStr) : boolean +>inf.isNumber : (x: string | number) => x is number +>inf : Inferrer +>isNumber : (x: string | number) => x is number +>numOrStr : string | number + + let t: number = numOrStr; // should ok +>t : number +>numOrStr : number + +} else { + let t: string = numOrStr; // should ok +>t : string +>numOrStr : string +} + +// Type predicates are not inferred on "this" +class C1 { +>C1 : C1 + + isC2() { +>isC2 : () => boolean + + return this instanceof C2; +>this instanceof C2 : boolean +>this : this +>C2 : typeof C2 + } +} +class C2 extends C1 { +>C2 : C2 +>C1 : C1 + + z = 0; +>z : number +>0 : 0 +} +declare let c: C1; +>c : C1 + +if (c.isC2()) { +>c.isC2() : boolean +>c.isC2 : () => boolean +>c : C1 +>isC2 : () => boolean + + let c2: C2 = c; // should error +>c2 : C2 +>c : C1 +} + +function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { +>doNotRefineDestructuredParam : ({ x, y }: { x: number | null; y: number;}) => boolean +>x : number | null +>y : number +>x : number | null +>y : number + + return typeof x === 'number'; +>typeof x === 'number' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : number | null +>'number' : "number" +} + diff --git a/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.errors.txt b/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.errors.txt deleted file mode 100644 index a4729d56ef55b..0000000000000 --- a/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.errors.txt +++ /dev/null @@ -1,30 +0,0 @@ -/src/a.js(10,7): error TS2417: Class static side 'typeof ElementsArray' incorrectly extends base class static side '{ isArray(arg: any): arg is any[]; readonly prototype: any[]; }'. - Types of property 'isArray' are incompatible. - Type '(arg: any) => boolean' is not assignable to type '(arg: any) => arg is any[]'. - Signature '(arg: any): boolean' must be a type predicate. - - -==== /src/a.js (1 errors) ==== - class Thing { - static { - this.doSomething = () => {}; - } - } - - Thing.doSomething(); - - // GH#46468 - class ElementsArray extends Array { - ~~~~~~~~~~~~~ -!!! error TS2417: Class static side 'typeof ElementsArray' incorrectly extends base class static side '{ isArray(arg: any): arg is any[]; readonly prototype: any[]; }'. -!!! error TS2417: Types of property 'isArray' are incompatible. -!!! error TS2417: Type '(arg: any) => boolean' is not assignable to type '(arg: any) => arg is any[]'. -!!! error TS2417: Signature '(arg: any): boolean' must be a type predicate. - static { - const superisArray = super.isArray; - const customIsArray = (arg)=> superisArray(arg); - this.isArray = customIsArray; - } - } - - ElementsArray.isArray(new ElementsArray()); \ No newline at end of file diff --git a/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.types b/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.types index d3e93bca00dde..878d7ab799230 100644 --- a/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.types +++ b/tests/baselines/reference/javascriptThisAssignmentInStaticBlock.types @@ -33,27 +33,27 @@ class ElementsArray extends Array { >isArray : (arg: any) => arg is any[] const customIsArray = (arg)=> superisArray(arg); ->customIsArray : (arg: any) => boolean ->(arg)=> superisArray(arg) : (arg: any) => boolean +>customIsArray : (arg: any) => arg is any[] +>(arg)=> superisArray(arg) : (arg: any) => arg is any[] >arg : any >superisArray(arg) : boolean >superisArray : (arg: any) => arg is any[] >arg : any this.isArray = customIsArray; ->this.isArray = customIsArray : (arg: any) => boolean ->this.isArray : (arg: any) => boolean +>this.isArray = customIsArray : (arg: any) => arg is any[] +>this.isArray : (arg: any) => arg is any[] >this : typeof ElementsArray ->isArray : (arg: any) => boolean ->customIsArray : (arg: any) => boolean +>isArray : (arg: any) => arg is any[] +>customIsArray : (arg: any) => arg is any[] } } ElementsArray.isArray(new ElementsArray()); >ElementsArray.isArray(new ElementsArray()) : boolean ->ElementsArray.isArray : (arg: any) => boolean +>ElementsArray.isArray : (arg: any) => arg is any[] >ElementsArray : typeof ElementsArray ->isArray : (arg: any) => boolean +>isArray : (arg: any) => arg is any[] >new ElementsArray() : ElementsArray >ElementsArray : typeof ElementsArray diff --git a/tests/baselines/reference/narrowByInstanceof.types b/tests/baselines/reference/narrowByInstanceof.types index 27900529f1fcc..a5d9169f25a23 100644 --- a/tests/baselines/reference/narrowByInstanceof.types +++ b/tests/baselines/reference/narrowByInstanceof.types @@ -94,7 +94,7 @@ class PersonMixin extends Function { >Function : Function public check(o: any) { ->check : (o: any) => boolean +>check : (o: any) => o is Person >o : any return typeof o === "object" && o !== null && o instanceof Person; diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts new file mode 100644 index 0000000000000..a4ca8136abd46 --- /dev/null +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -0,0 +1,211 @@ +// @strictNullChecks: true +// https://github.com/microsoft/TypeScript/issues/16069 + +const numsOrNull = [1, 2, 3, 4, null]; +const filteredNumsTruthy: number[] = numsOrNull.filter(x => !!x); // should error +const filteredNumsNonNullish: number[] = numsOrNull.filter(x => x !== null); // should ok + +const evenSquaresInline: number[] = // should error + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => !!x); // tests truthiness, not non-nullishness + +const isTruthy = (x: number | null) => !!x; + +const evenSquares: number[] = // should error + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(isTruthy); + +const evenSquaresNonNull: number[] = // should ok + [1, 2, 3, 4] + .map(x => x % 2 === 0 ? x * x : null) + .filter(x => x !== null); + +function isNonNull(x: number | null) { + return x !== null; +} + +// factoring out a boolean works thanks to aliased discriminants +function isNonNullVar(x: number | null) { + const ok = x !== null; + return ok; +} + +function isNonNullGeneric(x: T) { + return x !== null; +} + +// Type guards can flow between functions +const myGuard = (o: string | undefined): o is string => !!o; +const mySecondGuard = (o: string | undefined) => myGuard(o); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1327449914 +// This doesn't work because the false condition prevents type guard inference. +// Breaking up the filters does work. +type MyObj = { data?: string }; +type MyArray = { list?: MyObj[] }[]; +const myArray: MyArray = []; + +const result = myArray + .map((arr) => arr.list) + .filter((arr) => arr && arr.length) + .map((arr) => arr // should error + .filter((obj) => obj && obj.data) + .map(obj => JSON.parse(obj.data)) // should error + ); + +const result2 = myArray + .map((arr) => arr.list) + .filter((arr) => !!arr) + .filter(arr => arr.length) + .map((arr) => arr // should ok + .filter((obj) => obj) + // inferring a guard here would require https://github.com/microsoft/TypeScript/issues/42384 + .filter(obj => !!obj.data) + .map(obj => JSON.parse(obj.data)) + ); + +// https://github.com/microsoft/TypeScript/issues/16069#issuecomment-1183547889 +type Foo = { + foo: string; +} +type Bar = Foo & { + bar: string; +} + +const list: (Foo | Bar)[] = []; +const resultBars: Bar[] = list.filter((value) => 'bar' in value); // should ok + +function isBarNonNull(x: Foo | Bar | null) { + return ('bar' in x!); +} +const fooOrBar = list[0]; +if (isBarNonNull(fooOrBar)) { + const t: Bar = fooOrBar; // should ok +} + +// https://github.com/microsoft/TypeScript/issues/38390#issuecomment-626019466 +// Ryan's example (currently legal): +const a = [1, "foo", 2, "bar"].filter(x => typeof x === "string"); +a.push(10); + +// Defer to explicit type guards, even when they're incorrect. +function backwardsGuard(x: number|string): x is number { + return typeof x === 'string'; +} + +// Partition tests. The "false" case matters. +function isString(x: string | number) { + return typeof x === 'string'; +} + +declare let strOrNum: string | number; +if (isString(strOrNum)) { + let t: string = strOrNum; // should ok +} else { + let t: number = strOrNum; // should ok +} + +function flakyIsString(x: string | number) { + return typeof x === 'string' && Math.random() > 0.5; +} +if (flakyIsString(strOrNum)) { + let t: string = strOrNum; // should error +} else { + let t: number = strOrNum; // should error +} + +function isDate(x: object): x is Date { + return x instanceof Date; +} +function flakyIsDate(x: object): x is Date { + return x instanceof Date; +} + +declare let maybeDate: object; +if (isDate(maybeDate)) { + let t: Date = maybeDate; // should ok +} else { + let t: object = maybeDate; // should ok +} + +if (flakyIsDate(maybeDate)) { + let t: Date = maybeDate; // should ok +} else { + let t: object = maybeDate; // should ok +} + +// This should not infer a type guard since the value on which we do the refinement +// is not related to the original parameter. +function irrelevantIsNumber(x: string | number) { + x = Math.random() < 0.5 ? "string" : 123; + return typeof x === 'string'; +} +function irrelevantIsNumberDestructuring(x: string | number) { + [x] = [Math.random() < 0.5 ? "string" : 123]; + return typeof x === 'string'; +} + +// Cannot infer a type guard for either param because of the false case. +function areBothNums(x: string|number, y: string|number) { + return typeof x === 'number' && typeof y === 'number'; +} + +// Could potentially infer a type guard here but it would require more bookkeeping. +function doubleReturn(x: string|number) { + if (typeof x === 'string') { + return true; + } + return false; +} + +function guardsOneButNotOthers(a: string|number, b: string|number, c: string|number) { + return typeof b === 'string'; +} + +// String escaping issue (please help!) +function dunderguard(__x: number | string) { + return typeof __x === '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; + +// inferred guards in methods +interface NumberInferrer { + isNumber(x: number | string): x is number; +} +class Inferrer implements NumberInferrer { + isNumber(x: number | string) { // should ok + return typeof x === 'number'; + } +} +declare let numOrStr: number | string; +const inf = new Inferrer(); +if (inf.isNumber(numOrStr)) { + let t: number = numOrStr; // should ok +} else { + let t: string = numOrStr; // should ok +} + +// Type predicates are not inferred on "this" +class C1 { + isC2() { + return this instanceof C2; + } +} +class C2 extends C1 { + z = 0; +} +declare let c: C1; +if (c.isC2()) { + let c2: C2 = c; // should error +} + +function doNotRefineDestructuredParam({x, y}: {x: number | null, y: number}) { + return typeof x === 'number'; +} diff --git a/tests/cases/fourslash/thisPredicateFunctionQuickInfo.ts b/tests/cases/fourslash/thisPredicateFunctionQuickInfo.ts index 120a562607af8..cca30c4e8c8fb 100644 --- a/tests/cases/fourslash/thisPredicateFunctionQuickInfo.ts +++ b/tests/cases/fourslash/thisPredicateFunctionQuickInfo.ts @@ -65,5 +65,5 @@ verify.quickInfos({ 7: "(method) GuardInterface.isFollower(): this is FollowerGuard", 13: "let leaderStatus: boolean", 14: "let checkedLeaderStatus: boolean", - 15: "function isLeaderGuard(g: RoyalGuard): boolean" + 15: "function isLeaderGuard(g: RoyalGuard): g is LeadGuard" }); From e2684f128975dac725f4af4f9e6f03d4e765cfbe Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 21 Feb 2024 11:44:55 -0500 Subject: [PATCH 02/27] Run formatter --- src/compiler/checker.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6d75df43a1d3b..c95eeb18287a8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15462,11 +15462,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { signature.resolvedTypePredicate = type && isTypePredicateNode(type) ? createTypePredicateFromTypePredicateNode(type, signature) : jsdocPredicate || noTypePredicate; - } else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) { - const {declaration} = signature; - signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop + } + else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) { + const { declaration } = signature; + signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop signature.resolvedTypePredicate = getTypePredicateFromBody(declaration, signature) || noTypePredicate; - } else { + } + else { signature.resolvedTypePredicate = noTypePredicate; } } @@ -37405,7 +37407,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let singleReturn: Expression | undefined; if (func.body && func.body.kind !== SyntaxKind.Block) { singleReturn = func.body; // arrow function - } else { + } + else { if (functionHasImplicitReturn(func)) return undefined; const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => { @@ -37445,7 +37448,7 @@ 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 ?? { flags: FlowFlags.Start }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, node: expr, @@ -37462,7 +37465,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { 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)) { From 101df932d52904065eaec051239d25d7b2c40a5e Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 21 Feb 2024 16:16:15 -0500 Subject: [PATCH 03/27] Add secondary subtype check and tests --- src/compiler/checker.ts | 13 ++-- .../reference/inferTypePredicates.errors.txt | 21 ++++++ .../reference/inferTypePredicates.js | 40 +++++++++++ .../reference/inferTypePredicates.symbols | 53 ++++++++++++++ .../reference/inferTypePredicates.types | 70 +++++++++++++++++++ tests/cases/compiler/inferTypePredicates.ts | 30 ++++++++ 6 files changed, 223 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c95eeb18287a8..69e904183e6d9 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 ae38684f9f947..b170897623eff 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 0f4b992740c04..d8ebad0a117ce 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 cd633ff145cda..778ac6a6d3d3e 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 8764f712cbac9..7d90fb1f0b4f2 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 a4ca8136abd46..519701c9d6b27 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 +} From d0e385e8c781be33d05aaeccc7dcb594d230c296 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 21 Feb 2024 17:31:13 -0500 Subject: [PATCH 04/27] add union type test to baselines --- .../reference/inferTypePredicates.errors.txt | 9 ++++++ .../reference/inferTypePredicates.js | 16 ++++++++++ .../reference/inferTypePredicates.symbols | 21 +++++++++++++ .../reference/inferTypePredicates.types | 30 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index b170897623eff..ef4f5da02da96 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -275,4 +275,13 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 } 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 + } \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index d8ebad0a117ce..c0a694a6f9083 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -232,6 +232,15 @@ if (isStringFromUnknown(str)) { } 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 +} //// [inferTypePredicates.js] @@ -443,3 +452,10 @@ if (isStringFromUnknown(str)) { else { var t = str; // should OK } +// infer a union type +function isNumOrStr(x) { + return (typeof x === "number" || typeof x === "string"); +} +if (isNumOrStr(unk)) { + var t = unk; // should ok +} diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index 778ac6a6d3d3e..b5310e31b7b0a 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -659,3 +659,24 @@ if (isStringFromUnknown(str)) { >str : Symbol(str, Decl(inferTypePredicates.ts, 216, 11)) } +// infer a union type +function isNumOrStr(x: unknown) { +>isNumOrStr : Symbol(isNumOrStr, Decl(inferTypePredicates.ts, 230, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 233, 20)) + + return (typeof x === "number" || typeof x === "string"); +>x : Symbol(x, Decl(inferTypePredicates.ts, 233, 20)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 233, 20)) +} +declare let unk: unknown; +>unk : Symbol(unk, Decl(inferTypePredicates.ts, 236, 11)) + +if (isNumOrStr(unk)) { +>isNumOrStr : Symbol(isNumOrStr, Decl(inferTypePredicates.ts, 230, 1)) +>unk : Symbol(unk, Decl(inferTypePredicates.ts, 236, 11)) + + let t: number | string = unk; // should ok +>t : Symbol(t, Decl(inferTypePredicates.ts, 238, 5)) +>unk : Symbol(unk, Decl(inferTypePredicates.ts, 236, 11)) +} + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 7d90fb1f0b4f2..5252dd392f304 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -854,3 +854,33 @@ if (isStringFromUnknown(str)) { >str : never } +// infer a union type +function isNumOrStr(x: unknown) { +>isNumOrStr : (x: unknown) => x is string | number +>x : unknown + + return (typeof x === "number" || typeof x === "string"); +>(typeof x === "number" || typeof x === "string") : boolean +>typeof x === "number" || typeof x === "string" : boolean +>typeof x === "number" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>"number" : "number" +>typeof x === "string" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : unknown +>"string" : "string" +} +declare let unk: unknown; +>unk : unknown + +if (isNumOrStr(unk)) { +>isNumOrStr(unk) : boolean +>isNumOrStr : (x: unknown) => x is string | number +>unk : unknown + + let t: number | string = unk; // should ok +>t : string | number +>unk : string | number +} + From a72b1f17439f50a16b3b1ad162c6735f6e2b8db3 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 23 Feb 2024 10:50:48 -0500 Subject: [PATCH 05/27] add prisma circularity test with failing baseline --- .../circularConstructorWithReturn.errors.txt | 31 ++++++++++++ .../circularConstructorWithReturn.js | 41 ++++++++++++++++ .../circularConstructorWithReturn.symbols | 48 +++++++++++++++++++ .../circularConstructorWithReturn.types | 46 ++++++++++++++++++ .../compiler/circularConstructorWithReturn.ts | 18 +++++++ 5 files changed, 184 insertions(+) create mode 100644 tests/baselines/reference/circularConstructorWithReturn.errors.txt create mode 100644 tests/baselines/reference/circularConstructorWithReturn.js create mode 100644 tests/baselines/reference/circularConstructorWithReturn.symbols create mode 100644 tests/baselines/reference/circularConstructorWithReturn.types create mode 100644 tests/cases/compiler/circularConstructorWithReturn.ts diff --git a/tests/baselines/reference/circularConstructorWithReturn.errors.txt b/tests/baselines/reference/circularConstructorWithReturn.errors.txt new file mode 100644 index 0000000000000..ebc91184b5535 --- /dev/null +++ b/tests/baselines/reference/circularConstructorWithReturn.errors.txt @@ -0,0 +1,31 @@ +circularConstructorWithReturn.ts(3,13): error TS2456: Type alias 'Client' circularly references itself. +circularConstructorWithReturn.ts(7,5): error TS2502: 'self' is referenced directly or indirectly in its own type annotation. +circularConstructorWithReturn.ts(16,48): error TS2502: 'client' is referenced directly or indirectly in its own type annotation. + + +==== circularConstructorWithReturn.ts (3 errors) ==== + // This should not be a circularity error. See + // https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 + export type Client = ReturnType extends new () => infer T ? T : never + ~~~~~~ +!!! error TS2456: Type alias 'Client' circularly references itself. + + export function getPrismaClient(options?: any) { + class PrismaClient { + self: Client; + ~~~~ +!!! error TS2502: 'self' is referenced directly or indirectly in its own type annotation. + constructor(options?: any) { + return (this.self = applyModelsAndClientExtensions(this)); + } + } + + return PrismaClient + } + + export function applyModelsAndClientExtensions(client: Client) { + ~~~~~~~~~~~~~~ +!!! error TS2502: 'client' is referenced directly or indirectly in its own type annotation. + return client; + } + \ No newline at end of file diff --git a/tests/baselines/reference/circularConstructorWithReturn.js b/tests/baselines/reference/circularConstructorWithReturn.js new file mode 100644 index 0000000000000..b045d23c7e36f --- /dev/null +++ b/tests/baselines/reference/circularConstructorWithReturn.js @@ -0,0 +1,41 @@ +//// [tests/cases/compiler/circularConstructorWithReturn.ts] //// + +//// [circularConstructorWithReturn.ts] +// This should not be a circularity error. See +// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 +export type Client = ReturnType extends new () => infer T ? T : never + +export function getPrismaClient(options?: any) { + class PrismaClient { + self: Client; + constructor(options?: any) { + return (this.self = applyModelsAndClientExtensions(this)); + } + } + + return PrismaClient +} + +export function applyModelsAndClientExtensions(client: Client) { + return client; +} + + +//// [circularConstructorWithReturn.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.applyModelsAndClientExtensions = exports.getPrismaClient = void 0; +function getPrismaClient(options) { + var PrismaClient = /** @class */ (function () { + function PrismaClient(options) { + return (this.self = applyModelsAndClientExtensions(this)); + } + return PrismaClient; + }()); + return PrismaClient; +} +exports.getPrismaClient = getPrismaClient; +function applyModelsAndClientExtensions(client) { + return client; +} +exports.applyModelsAndClientExtensions = applyModelsAndClientExtensions; diff --git a/tests/baselines/reference/circularConstructorWithReturn.symbols b/tests/baselines/reference/circularConstructorWithReturn.symbols new file mode 100644 index 0000000000000..52cb36b6457a8 --- /dev/null +++ b/tests/baselines/reference/circularConstructorWithReturn.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/compiler/circularConstructorWithReturn.ts] //// + +=== circularConstructorWithReturn.ts === +// This should not be a circularity error. See +// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 +export type Client = ReturnType extends new () => infer T ? T : never +>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0)) +>ReturnType : Symbol(ReturnType, Decl(lib.es5.d.ts, --, --)) +>getPrismaClient : Symbol(getPrismaClient, Decl(circularConstructorWithReturn.ts, 2, 93)) +>T : Symbol(T, Decl(circularConstructorWithReturn.ts, 2, 79)) +>T : Symbol(T, Decl(circularConstructorWithReturn.ts, 2, 79)) + +export function getPrismaClient(options?: any) { +>getPrismaClient : Symbol(getPrismaClient, Decl(circularConstructorWithReturn.ts, 2, 93)) +>options : Symbol(options, Decl(circularConstructorWithReturn.ts, 4, 32)) + + class PrismaClient { +>PrismaClient : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48)) + + self: Client; +>self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22)) +>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0)) + + constructor(options?: any) { +>options : Symbol(options, Decl(circularConstructorWithReturn.ts, 7, 16)) + + return (this.self = applyModelsAndClientExtensions(this)); +>this.self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22)) +>this : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48)) +>self : Symbol(PrismaClient.self, Decl(circularConstructorWithReturn.ts, 5, 22)) +>applyModelsAndClientExtensions : Symbol(applyModelsAndClientExtensions, Decl(circularConstructorWithReturn.ts, 13, 1)) +>this : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48)) + } + } + + return PrismaClient +>PrismaClient : Symbol(PrismaClient, Decl(circularConstructorWithReturn.ts, 4, 48)) +} + +export function applyModelsAndClientExtensions(client: Client) { +>applyModelsAndClientExtensions : Symbol(applyModelsAndClientExtensions, Decl(circularConstructorWithReturn.ts, 13, 1)) +>client : Symbol(client, Decl(circularConstructorWithReturn.ts, 15, 47)) +>Client : Symbol(Client, Decl(circularConstructorWithReturn.ts, 0, 0)) + + return client; +>client : Symbol(client, Decl(circularConstructorWithReturn.ts, 15, 47)) +} + diff --git a/tests/baselines/reference/circularConstructorWithReturn.types b/tests/baselines/reference/circularConstructorWithReturn.types new file mode 100644 index 0000000000000..72fb4aaa7328a --- /dev/null +++ b/tests/baselines/reference/circularConstructorWithReturn.types @@ -0,0 +1,46 @@ +//// [tests/cases/compiler/circularConstructorWithReturn.ts] //// + +=== circularConstructorWithReturn.ts === +// This should not be a circularity error. See +// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 +export type Client = ReturnType extends new () => infer T ? T : never +>Client : any +>getPrismaClient : (options?: any) => typeof PrismaClient + +export function getPrismaClient(options?: any) { +>getPrismaClient : (options?: any) => typeof PrismaClient +>options : any + + class PrismaClient { +>PrismaClient : PrismaClient + + self: Client; +>self : any + + constructor(options?: any) { +>options : any + + return (this.self = applyModelsAndClientExtensions(this)); +>(this.self = applyModelsAndClientExtensions(this)) : any +>this.self = applyModelsAndClientExtensions(this) : any +>this.self : any +>this : this +>self : any +>applyModelsAndClientExtensions(this) : any +>applyModelsAndClientExtensions : (client: any) => any +>this : this + } + } + + return PrismaClient +>PrismaClient : typeof PrismaClient +} + +export function applyModelsAndClientExtensions(client: Client) { +>applyModelsAndClientExtensions : (client: any) => any +>client : any + + return client; +>client : any +} + diff --git a/tests/cases/compiler/circularConstructorWithReturn.ts b/tests/cases/compiler/circularConstructorWithReturn.ts new file mode 100644 index 0000000000000..ca65908103816 --- /dev/null +++ b/tests/cases/compiler/circularConstructorWithReturn.ts @@ -0,0 +1,18 @@ +// This should not be a circularity error. See +// https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 +export type Client = ReturnType extends new () => infer T ? T : never + +export function getPrismaClient(options?: any) { + class PrismaClient { + self: Client; + constructor(options?: any) { + return (this.self = applyModelsAndClientExtensions(this)); + } + } + + return PrismaClient +} + +export function applyModelsAndClientExtensions(client: Client) { + return client; +} From ef2d4650473493b3f26a0b06dfa6fba1135b1dfe Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 23 Feb 2024 10:51:44 -0500 Subject: [PATCH 06/27] Various fixes for circularity issue --- src/compiler/checker.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 69e904183e6d9..33465ce7a2c61 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15466,7 +15466,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else if (signature.declaration && isFunctionLikeDeclaration(signature.declaration) && (!signature.resolvedReturnType || signature.resolvedReturnType === booleanType)) { const { declaration } = signature; signature.resolvedTypePredicate = noTypePredicate; // avoid infinite loop - signature.resolvedTypePredicate = getTypePredicateFromBody(declaration, signature) || noTypePredicate; + signature.resolvedTypePredicate = getTypePredicateFromBody(declaration) || noTypePredicate; } else { signature.resolvedTypePredicate = noTypePredicate; @@ -37399,9 +37399,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } - function getTypePredicateFromBody(func: FunctionLikeDeclaration, _sig: Signature): TypePredicate | undefined { + function getTypePredicateFromBody(func: FunctionLikeDeclaration): TypePredicate | undefined { + switch (func.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return undefined; + } const functionFlags = getFunctionFlags(func); - if (functionFlags !== FunctionFlags.Normal) return undefined; + if (functionFlags !== FunctionFlags.Normal || func.parameters.length === 0) return undefined; // Only attempt to infer a type predicate if there's exactly one return. let singleReturn: Expression | undefined; @@ -37418,6 +37424,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (bailedEarly || !singleReturn) return undefined; } + if (isTriviallyNonBoolean(singleReturn)) return undefined; + const predicate = checkIfExpressionRefinesAnyParameter(singleReturn); if (predicate) { const [i, type] = predicate; @@ -37431,7 +37439,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined { expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - const type = checkExpressionCached(expr, CheckMode.TypeOnly); + const type = checkExpressionCached(expr); // , CheckMode.TypeOnly); if (type !== booleanType || !func.body) return undefined; return forEach(func.parameters, (param, i) => { @@ -37477,6 +37485,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return trueType; } } + + // 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; + } + } + return false; // may or may not be boolean + } } /** From 933605267cc9da80babde89c10acff3247ebe6b4 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 23 Feb 2024 10:52:40 -0500 Subject: [PATCH 07/27] circularity test is fixed --- .../circularConstructorWithReturn.errors.txt | 31 ------------------- .../circularConstructorWithReturn.types | 22 ++++++------- 2 files changed, 11 insertions(+), 42 deletions(-) delete mode 100644 tests/baselines/reference/circularConstructorWithReturn.errors.txt diff --git a/tests/baselines/reference/circularConstructorWithReturn.errors.txt b/tests/baselines/reference/circularConstructorWithReturn.errors.txt deleted file mode 100644 index ebc91184b5535..0000000000000 --- a/tests/baselines/reference/circularConstructorWithReturn.errors.txt +++ /dev/null @@ -1,31 +0,0 @@ -circularConstructorWithReturn.ts(3,13): error TS2456: Type alias 'Client' circularly references itself. -circularConstructorWithReturn.ts(7,5): error TS2502: 'self' is referenced directly or indirectly in its own type annotation. -circularConstructorWithReturn.ts(16,48): error TS2502: 'client' is referenced directly or indirectly in its own type annotation. - - -==== circularConstructorWithReturn.ts (3 errors) ==== - // This should not be a circularity error. See - // https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 - export type Client = ReturnType extends new () => infer T ? T : never - ~~~~~~ -!!! error TS2456: Type alias 'Client' circularly references itself. - - export function getPrismaClient(options?: any) { - class PrismaClient { - self: Client; - ~~~~ -!!! error TS2502: 'self' is referenced directly or indirectly in its own type annotation. - constructor(options?: any) { - return (this.self = applyModelsAndClientExtensions(this)); - } - } - - return PrismaClient - } - - export function applyModelsAndClientExtensions(client: Client) { - ~~~~~~~~~~~~~~ -!!! error TS2502: 'client' is referenced directly or indirectly in its own type annotation. - return client; - } - \ No newline at end of file diff --git a/tests/baselines/reference/circularConstructorWithReturn.types b/tests/baselines/reference/circularConstructorWithReturn.types index 72fb4aaa7328a..53e59755f3942 100644 --- a/tests/baselines/reference/circularConstructorWithReturn.types +++ b/tests/baselines/reference/circularConstructorWithReturn.types @@ -4,7 +4,7 @@ // This should not be a circularity error. See // https://github.com/microsoft/TypeScript/pull/57465#issuecomment-1960271216 export type Client = ReturnType extends new () => infer T ? T : never ->Client : any +>Client : PrismaClient >getPrismaClient : (options?: any) => typeof PrismaClient export function getPrismaClient(options?: any) { @@ -15,19 +15,19 @@ export function getPrismaClient(options?: any) { >PrismaClient : PrismaClient self: Client; ->self : any +>self : PrismaClient constructor(options?: any) { >options : any return (this.self = applyModelsAndClientExtensions(this)); ->(this.self = applyModelsAndClientExtensions(this)) : any ->this.self = applyModelsAndClientExtensions(this) : any ->this.self : any +>(this.self = applyModelsAndClientExtensions(this)) : PrismaClient +>this.self = applyModelsAndClientExtensions(this) : PrismaClient +>this.self : PrismaClient >this : this ->self : any ->applyModelsAndClientExtensions(this) : any ->applyModelsAndClientExtensions : (client: any) => any +>self : PrismaClient +>applyModelsAndClientExtensions(this) : PrismaClient +>applyModelsAndClientExtensions : (client: PrismaClient) => PrismaClient >this : this } } @@ -37,10 +37,10 @@ export function getPrismaClient(options?: any) { } export function applyModelsAndClientExtensions(client: Client) { ->applyModelsAndClientExtensions : (client: any) => any ->client : any +>applyModelsAndClientExtensions : (client: Client) => PrismaClient +>client : PrismaClient return client; ->client : any +>client : PrismaClient } From 41f624d67eaf81702d79a5077fc559f4fdb22121 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 23 Feb 2024 11:13:59 -0500 Subject: [PATCH 08/27] revert back to CheckMode.TypeOnly --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 33465ce7a2c61..18fc82e8f5770 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37439,7 +37439,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined { expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - const type = checkExpressionCached(expr); // , CheckMode.TypeOnly); + const type = checkExpressionCached(expr, CheckMode.TypeOnly); if (type !== booleanType || !func.body) return undefined; return forEach(func.parameters, (param, i) => { From 52df1158d9497a449c79943c9956e9a1b5719768 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 23 Feb 2024 11:14:14 -0500 Subject: [PATCH 09/27] Add test case for a predicate that throws --- .../reference/inferTypePredicates.errors.txt | 15 ++++++++ .../reference/inferTypePredicates.js | 28 ++++++++++++++ .../reference/inferTypePredicates.symbols | 34 +++++++++++++++++ .../reference/inferTypePredicates.types | 38 +++++++++++++++++++ tests/cases/compiler/inferTypePredicates.ts | 15 ++++++++ 5 files changed, 130 insertions(+) diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index ef4f5da02da96..b8ef2508d3b6b 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -284,4 +284,19 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 if (isNumOrStr(unk)) { let t: number | string = unk; // should ok } + + // A function can be a type predicate even if it throws. + function assertAndPredicate(x: string | number | Date) { + if (x instanceof Date) { + throw new Error(); + } + return typeof x === 'string'; + } + + declare let snd: string | number | Date; + if (assertAndPredicate(snd)) { + let t: string = snd; // should ok + } else { + snd; // type is number | Date + } \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index c0a694a6f9083..94b5ed4d8ad42 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -241,6 +241,21 @@ declare let unk: unknown; if (isNumOrStr(unk)) { let t: number | string = unk; // should ok } + +// A function can be a type predicate even if it throws. +function assertAndPredicate(x: string | number | Date) { + if (x instanceof Date) { + throw new Error(); + } + return typeof x === 'string'; +} + +declare let snd: string | number | Date; +if (assertAndPredicate(snd)) { + let t: string = snd; // should ok +} else { + snd; // type is number | Date +} //// [inferTypePredicates.js] @@ -459,3 +474,16 @@ function isNumOrStr(x) { if (isNumOrStr(unk)) { var t = unk; // should ok } +// A function can be a type predicate even if it throws. +function assertAndPredicate(x) { + if (x instanceof Date) { + throw new Error(); + } + return typeof x === 'string'; +} +if (assertAndPredicate(snd)) { + var t = snd; // should ok +} +else { + snd; // type is number | Date +} diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index b5310e31b7b0a..adb871430b486 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -680,3 +680,37 @@ if (isNumOrStr(unk)) { >unk : Symbol(unk, Decl(inferTypePredicates.ts, 236, 11)) } +// A function can be a type predicate even if it throws. +function assertAndPredicate(x: string | number | Date) { +>assertAndPredicate : Symbol(assertAndPredicate, Decl(inferTypePredicates.ts, 239, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 242, 28)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) + + if (x instanceof Date) { +>x : Symbol(x, Decl(inferTypePredicates.ts, 242, 28)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) + + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + return typeof x === 'string'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 242, 28)) +} + +declare let snd: string | number | Date; +>snd : Symbol(snd, Decl(inferTypePredicates.ts, 249, 11)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) + +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 +>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)) +} + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 5252dd392f304..27206d902095d 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -884,3 +884,41 @@ if (isNumOrStr(unk)) { >unk : string | number } +// 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 +>x : string | number | Date + + if (x instanceof Date) { +>x instanceof Date : boolean +>x : string | number | Date +>Date : DateConstructor + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } + return typeof x === 'string'; +>typeof x === 'string' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | number +>'string' : "string" +} + +declare let snd: string | number | Date; +>snd : string | number | Date + +if (assertAndPredicate(snd)) { +>assertAndPredicate(snd) : boolean +>assertAndPredicate : (x: string | number | Date) => x is string +>snd : string | number | Date + + let t: string = snd; // should ok +>t : string +>snd : string + +} else { + snd; // type is number | Date +>snd : number | Date +} + diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index 519701c9d6b27..388f38910f508 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -239,3 +239,18 @@ declare let unk: unknown; if (isNumOrStr(unk)) { let t: number | string = unk; // should ok } + +// A function can be a type predicate even if it throws. +function assertAndPredicate(x: string | number | Date) { + if (x instanceof Date) { + throw new Error(); + } + return typeof x === 'string'; +} + +declare let snd: string | number | Date; +if (assertAndPredicate(snd)) { + let t: string = snd; // should ok +} else { + snd; // type is number | Date +} From 3ab6faef39b67169ff4cab5fc96a467bf81b2eb1 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sun, 25 Feb 2024 19:08:00 -0500 Subject: [PATCH 10/27] Drop isTriviallyNonBoolean, switch to simpler test, check for assertions --- src/compiler/checker.ts | 55 +++++++------------ .../reference/inferTypePredicates.errors.txt | 19 +++++-- .../reference/inferTypePredicates.js | 22 ++++---- .../reference/inferTypePredicates.symbols | 18 ++++-- .../reference/inferTypePredicates.types | 34 +++++++----- tests/cases/compiler/inferTypePredicates.ts | 12 ++-- 6 files changed, 86 insertions(+), 74 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 18fc82e8f5770..307b08503921a 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; - } - } - - // 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; + 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 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 b8ef2508d3b6b..e9cf945b5bd33 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 94b5ed4d8ad42..57bb817194b26 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 adb871430b486..b8678864296d9 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 27206d902095d..3e8cfb62bc51d 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 +>snd : string | number | Date +} -} else { - snd; // type is number | Date ->snd : 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 388f38910f508..42fdbccf1a48d 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'; } From 9591231ded91b2d6ae2c518677c9983c7952eca1 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Thu, 29 Feb 2024 13:30:55 -0500 Subject: [PATCH 11/27] Use unescapeLeadingUnderscores --- src/compiler/checker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 307b08503921a..6790bcc8f0f1a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37431,8 +37431,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; From 9a8c0a11817d0a506656c0b99e99ff80807046f9 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Thu, 29 Feb 2024 13:32:56 -0500 Subject: [PATCH 12/27] tests are fixed --- tests/baselines/reference/inferTypePredicates.errors.txt | 2 +- tests/baselines/reference/inferTypePredicates.js | 4 ++-- tests/baselines/reference/inferTypePredicates.symbols | 2 +- tests/baselines/reference/inferTypePredicates.types | 4 ++-- tests/cases/compiler/inferTypePredicates.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index e9cf945b5bd33..c000e684e9360 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -208,7 +208,7 @@ inferTypePredicates.ts(252,7): error TS2322: Type 'string | number | Date' is no return typeof b === 'string'; } - // String escaping issue (please help!) + // Checks that there are no string escaping issues function dunderguard(__x: number | string) { return typeof __x === 'string'; } diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index 57bb817194b26..29bcf8a0cf47e 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -166,7 +166,7 @@ function guardsOneButNotOthers(a: string|number, b: string|number, c: string|num return typeof b === 'string'; } -// String escaping issue (please help!) +// Checks that there are no string escaping issues function dunderguard(__x: number | string) { return typeof __x === 'string'; } @@ -402,7 +402,7 @@ function doubleReturn(x) { function guardsOneButNotOthers(a, b, c) { return typeof b === 'string'; } -// String escaping issue (please help!) +// Checks that there are no string escaping issues function dunderguard(__x) { return typeof __x === 'string'; } diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index b8678864296d9..c412075f07273 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -495,7 +495,7 @@ function guardsOneButNotOthers(a: string|number, b: string|number, c: string|num >b : Symbol(b, Decl(inferTypePredicates.ts, 161, 48)) } -// String escaping issue (please help!) +// Checks that there are no string escaping issues function dunderguard(__x: number | string) { >dunderguard : Symbol(dunderguard, Decl(inferTypePredicates.ts, 163, 1)) >__x : Symbol(__x, Decl(inferTypePredicates.ts, 166, 21)) diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 3e8cfb62bc51d..c964d9d329a4d 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -659,9 +659,9 @@ function guardsOneButNotOthers(a: string|number, b: string|number, c: string|num >'string' : "string" } -// String escaping issue (please help!) +// Checks that there are no string escaping issues 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'; diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index 42fdbccf1a48d..df2728b6c3d28 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -164,7 +164,7 @@ function guardsOneButNotOthers(a: string|number, b: string|number, c: string|num return typeof b === 'string'; } -// String escaping issue (please help!) +// Checks that there are no string escaping issues function dunderguard(__x: number | string) { return typeof __x === 'string'; } From a4ff6b4d87a5b18a340b24d5454b119c7af48856 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 29 Feb 2024 16:19:27 -0800 Subject: [PATCH 13/27] 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 7e9f3ced07fec..081bb1fb5e9ba 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 307b08503921a..095ba1a7b9426 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 14/27] 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 e9cf945b5bd33..2789e3c3b8754 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 3e8cfb62bc51d..69fb011dea268 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) { From 0dec9c67b51e6848b3720ad9d58ce8fd31daca94 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 1 Mar 2024 11:57:57 -0500 Subject: [PATCH 15/27] simplify --- src/compiler/checker.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 095ba1a7b9426..4f7e8d1a6f71d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37424,30 +37424,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (bailedEarly || !singleReturn) return undefined; } - const predicate = checkIfExpressionRefinesAnyParameter(singleReturn); - if (predicate) { - const [i, type] = predicate; - const param = func.parameters[i]; - if (isIdentifier(param.name)) { - return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, type); - } - } - return undefined; + return checkIfExpressionRefinesAnyParameter(singleReturn); - function checkIfExpressionRefinesAnyParameter(expr: Expression): [number, Type] | undefined { + function checkIfExpressionRefinesAnyParameter(expr: Expression): TypePredicate | undefined { expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - const type = checkExpressionCached(expr); - if (!(type.flags & TypeFlags.Boolean)) return undefined; + const returnType = checkExpressionCached(expr); + if (!(returnType.flags & TypeFlags.Boolean)) return undefined; return forEach(func.parameters, (param, i) => { const initType = getTypeOfSymbol(param.symbol); - if (!initType || initType.flags & TypeFlags.Boolean || isSymbolAssigned(param.symbol)) { + if (!initType || initType.flags & TypeFlags.Boolean || !isIdentifier(param.name) || isSymbolAssigned(param.symbol)) { // Refining "x: boolean" to "x is true" or "x is false" isn't useful. return; } const trueType = checkIfExpressionRefinesParameter(expr, param, initType); if (trueType) { - return [i, trueType]; + return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, trueType); } }); } From 703253ae6b95b9731649ca7cc2529099e5115007 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 1 Mar 2024 17:37:23 -0800 Subject: [PATCH 16/27] Delay expensive functionHasImplicitReturn call --- src/compiler/checker.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4f7e8d1a6f71d..1f69b357ed051 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37415,13 +37415,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { singleReturn = func.body; // arrow function } else { - if (functionHasImplicitReturn(func)) return undefined; - const bailedEarly = forEachReturnStatement(func.body as Block, returnStatement => { if (singleReturn || !returnStatement.expression) return true; singleReturn = returnStatement.expression; }); - if (bailedEarly || !singleReturn) return undefined; + if (bailedEarly || !singleReturn || functionHasImplicitReturn(func)) return undefined; } return checkIfExpressionRefinesAnyParameter(singleReturn); From c7f1c3d309b3b525cba7e17c4388e9d8ec2588a5 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 1 Mar 2024 21:40:28 -0800 Subject: [PATCH 17/27] Avoid creating closures --- src/compiler/checker.ts | 75 ++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1f69b357ed051..78bfd88acf7f3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37421,49 +37421,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }); if (bailedEarly || !singleReturn || functionHasImplicitReturn(func)) return undefined; } + return checkIfExpressionRefinesAnyParameter(func, singleReturn); + } - return checkIfExpressionRefinesAnyParameter(singleReturn); - - function checkIfExpressionRefinesAnyParameter(expr: Expression): TypePredicate | undefined { - expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); - const returnType = checkExpressionCached(expr); - if (!(returnType.flags & TypeFlags.Boolean)) return undefined; + function checkIfExpressionRefinesAnyParameter(func: FunctionLikeDeclaration, expr: Expression): TypePredicate | undefined { + expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); + const returnType = checkExpressionCached(expr); + if (!(returnType.flags & TypeFlags.Boolean)) return undefined; - return forEach(func.parameters, (param, i) => { - const initType = getTypeOfSymbol(param.symbol); - if (!initType || initType.flags & TypeFlags.Boolean || !isIdentifier(param.name) || isSymbolAssigned(param.symbol)) { - // Refining "x: boolean" to "x is true" or "x is false" isn't useful. - return; - } - const trueType = checkIfExpressionRefinesParameter(expr, param, initType); - if (trueType) { - return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, trueType); - } - }); - } + return forEach(func.parameters, (param, i) => { + const initType = getTypeOfSymbol(param.symbol); + if (!initType || initType.flags & TypeFlags.Boolean || !isIdentifier(param.name) || isSymbolAssigned(param.symbol)) { + // Refining "x: boolean" to "x is true" or "x is false" isn't useful. + return; + } + const trueType = checkIfExpressionRefinesParameter(func, expr, param, initType); + if (trueType) { + return createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), i, trueType); + } + }); + } - function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { - 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, - antecedent, - }; + function checkIfExpressionRefinesParameter(func: FunctionLikeDeclaration, expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { + 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, + antecedent, + }; - const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType) return undefined; + const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); + 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. - // 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 falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); - return falseSubtype.flags & TypeFlags.Never ? trueType : 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. + // 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 falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); + return falseSubtype.flags & TypeFlags.Never ? trueType : undefined; } /** From 25743a3a989a17321e4994552c82815d4795ab27 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 2 Mar 2024 16:54:49 -0500 Subject: [PATCH 18/27] Add fallback isTypeAssignableTo check to test for equivalence --- src/compiler/checker.ts | 5 ++++- .../reference/inferTypePredicates.errors.txt | 4 ++++ tests/baselines/reference/inferTypePredicates.js | 7 +++++++ tests/baselines/reference/inferTypePredicates.symbols | 8 ++++++++ tests/baselines/reference/inferTypePredicates.types | 11 +++++++++++ tests/cases/compiler/inferTypePredicates.ts | 4 ++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4f7e8d1a6f71d..edca035764646 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37455,7 +37455,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType) return undefined; + // Since trueType is a subtype of initType, isTypeAssignableTo(initType, trueType) is a test for equivalence (modulo any types). + if (trueType === initType || ((initType.flags & TypeFlags.Any) ? (trueType.flags & TypeFlags.Any) : isTypeAssignableTo(initType, trueType))) { + 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. // 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`. diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index 440b668c7ef61..676b69dcbbc34 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -301,4 +301,8 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 function isNumberWithThis(this: Date, x: number | string) { return typeof x === 'number'; } + + function narrowFromAny(x: any) { + 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 29bcf8a0cf47e..4301fc33584eb 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -258,6 +258,10 @@ if (assertAndPredicate(snd)) { function isNumberWithThis(this: Date, x: number | string) { return typeof x === 'number'; } + +function narrowFromAny(x: any) { + return typeof x === 'number'; +} //// [inferTypePredicates.js] @@ -489,3 +493,6 @@ if (assertAndPredicate(snd)) { function isNumberWithThis(x) { return typeof x === 'number'; } +function narrowFromAny(x) { + return typeof x === 'number'; +} diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index c412075f07273..4708b87e323cb 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -720,3 +720,11 @@ function isNumberWithThis(this: Date, x: number | string) { >x : Symbol(x, Decl(inferTypePredicates.ts, 254, 37)) } +function narrowFromAny(x: any) { +>narrowFromAny : Symbol(narrowFromAny, Decl(inferTypePredicates.ts, 256, 1)) +>x : Symbol(x, Decl(inferTypePredicates.ts, 258, 23)) + + return typeof x === 'number'; +>x : Symbol(x, Decl(inferTypePredicates.ts, 258, 23)) +} + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 4564daa438851..669933a1c8579 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -930,3 +930,14 @@ function isNumberWithThis(this: Date, x: number | string) { >'number' : "number" } +function narrowFromAny(x: any) { +>narrowFromAny : (x: any) => x is number +>x : any + + return typeof x === 'number'; +>typeof x === 'number' : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : any +>'number' : "number" +} + diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index df2728b6c3d28..0cf620e194123 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -256,3 +256,7 @@ if (assertAndPredicate(snd)) { function isNumberWithThis(this: Date, x: number | string) { return typeof x === 'number'; } + +function narrowFromAny(x: any) { + return typeof x === 'number'; +} From 4e79d761634db1e0c54c3b1acf3ac315be034042 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 2 Mar 2024 19:04:34 -0500 Subject: [PATCH 19/27] try caching the antecedent --- src/compiler/checker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index edca035764646..a86494e7440c8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37445,13 +37445,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { - const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || + const baseAntecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode || { flags: FlowFlags.Start }; + const sharedAntecedent = { ...baseAntecedent, flags: baseAntecedent.flags & FlowFlags.Shared }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, node: expr, - antecedent, + antecedent: sharedAntecedent, }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); From a5725d28f151c9b16ff1e527dcc1374a5f711994 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sun, 3 Mar 2024 10:01:14 -0500 Subject: [PATCH 20/27] revert fallback assignability check --- src/compiler/checker.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a86494e7440c8..031238bc22106 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37456,10 +37456,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - // Since trueType is a subtype of initType, isTypeAssignableTo(initType, trueType) is a test for equivalence (modulo any types). - if (trueType === initType || ((initType.flags & TypeFlags.Any) ? (trueType.flags & TypeFlags.Any) : isTypeAssignableTo(initType, trueType))) { - 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. // 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`. From 332357320c471add3b7f67e5e703500467a3e35d Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Mon, 4 Mar 2024 08:28:12 -0500 Subject: [PATCH 21/27] Revert "try caching the antecedent" This reverts commit 4e79d761634db1e0c54c3b1acf3ac315be034042. --- src/compiler/checker.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 031238bc22106..4f7e8d1a6f71d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37445,18 +37445,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function checkIfExpressionRefinesParameter(expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { - const baseAntecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || + const antecedent = (expr as Expression & { flowNode?: FlowNode; }).flowNode || expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode || { flags: FlowFlags.Start }; - const sharedAntecedent = { ...baseAntecedent, flags: baseAntecedent.flags & FlowFlags.Shared }; const trueCondition: FlowCondition = { flags: FlowFlags.TrueCondition, node: expr, - antecedent: sharedAntecedent, + antecedent, }; const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType) 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. // 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`. From 8edd1199d76eb401d95e2e7887020e39151c2651 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 5 Mar 2024 18:02:41 -0500 Subject: [PATCH 22/27] implement the never bailout --- src/compiler/checker.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 78bfd88acf7f3..146687fb8c53b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37461,8 +37461,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ...trueCondition, flags: FlowFlags.FalseCondition, }; - const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); - return falseSubtype.flags & TypeFlags.Never ? trueType : undefined; + + let isNever: boolean; + if (trueType.flags & TypeFlags.Union) { + isNever = true; + mapType(trueType, t => { + if (!isNever) return neverType; + const falseSubT = getFlowTypeOfReference(param.name, t, t, func, falseCondition); + if (!(falseSubT.flags & TypeFlags.Never)) { + isNever = false; + } + return neverType; + }); + } else { + const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); + isNever = !!(falseSubtype.flags & TypeFlags.Never); + } + return isNever ? trueType : undefined; } /** From 9479ed89ec646a4d98013d367f0a7b195d5a40eb Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 5 Mar 2024 18:18:45 -0500 Subject: [PATCH 23/27] attempt to implement === fast path --- src/compiler/checker.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 146687fb8c53b..bfb5b4dc3ba86 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37452,6 +37452,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { antecedent, }; + if (isBinaryExpression(expr)) { + const {operatorToken} = expr; + const operator = operatorToken.kind; + let target; + switch (operator) { + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + const {left, right} = expr; + if (isMatchingReference(param.name, left)) { + target = right; + } else if (isMatchingReference(param.name, right)) { + target = left; + } + } + if (target) { + const targetType = getTypeOfExpression(target); + if (!isUnitType(targetType)) { + return undefined; + } + } + } + const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); if (trueType === initType) return undefined; From f8338651908d17100ab42a4973193f70d33d0cfe Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Tue, 5 Mar 2024 18:21:36 -0500 Subject: [PATCH 24/27] add === test and baselines --- .../reference/inferTypePredicates.errors.txt | 5 +++++ .../reference/inferTypePredicates.js | 6 ++++++ .../reference/inferTypePredicates.symbols | 18 ++++++++++++++++++ .../reference/inferTypePredicates.types | 19 +++++++++++++++++++ tests/cases/compiler/inferTypePredicates.ts | 5 +++++ 5 files changed, 53 insertions(+) diff --git a/tests/baselines/reference/inferTypePredicates.errors.txt b/tests/baselines/reference/inferTypePredicates.errors.txt index 676b69dcbbc34..0598c0a8440bd 100644 --- a/tests/baselines/reference/inferTypePredicates.errors.txt +++ b/tests/baselines/reference/inferTypePredicates.errors.txt @@ -305,4 +305,9 @@ inferTypePredicates.ts(205,7): error TS2741: Property 'z' is missing in type 'C1 function narrowFromAny(x: any) { return typeof x === 'number'; } + + // test fast path for '===' (which is usually not a type predicate) + declare let needle: string | number | boolean; + declare let haystack: (string | number | Date | RegExp)[]; + const finds = haystack.filter(n => n === needle); \ No newline at end of file diff --git a/tests/baselines/reference/inferTypePredicates.js b/tests/baselines/reference/inferTypePredicates.js index 4301fc33584eb..d2f21cfde401b 100644 --- a/tests/baselines/reference/inferTypePredicates.js +++ b/tests/baselines/reference/inferTypePredicates.js @@ -262,6 +262,11 @@ function isNumberWithThis(this: Date, x: number | string) { function narrowFromAny(x: any) { return typeof x === 'number'; } + +// test fast path for '===' (which is usually not a type predicate) +declare let needle: string | number | boolean; +declare let haystack: (string | number | Date | RegExp)[]; +const finds = haystack.filter(n => n === needle); //// [inferTypePredicates.js] @@ -496,3 +501,4 @@ function isNumberWithThis(x) { function narrowFromAny(x) { return typeof x === 'number'; } +var finds = haystack.filter(function (n) { return n === needle; }); diff --git a/tests/baselines/reference/inferTypePredicates.symbols b/tests/baselines/reference/inferTypePredicates.symbols index 4708b87e323cb..3d55c1910f01a 100644 --- a/tests/baselines/reference/inferTypePredicates.symbols +++ b/tests/baselines/reference/inferTypePredicates.symbols @@ -728,3 +728,21 @@ function narrowFromAny(x: any) { >x : Symbol(x, Decl(inferTypePredicates.ts, 258, 23)) } +// test fast path for '===' (which is usually not a type predicate) +declare let needle: string | number | boolean; +>needle : Symbol(needle, Decl(inferTypePredicates.ts, 263, 11)) + +declare let haystack: (string | number | Date | RegExp)[]; +>haystack : Symbol(haystack, Decl(inferTypePredicates.ts, 264, 11)) +>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --)) +>RegExp : Symbol(RegExp, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + +const finds = haystack.filter(n => n === needle); +>finds : Symbol(finds, Decl(inferTypePredicates.ts, 265, 5)) +>haystack.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>haystack : Symbol(haystack, Decl(inferTypePredicates.ts, 264, 11)) +>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>n : Symbol(n, Decl(inferTypePredicates.ts, 265, 30)) +>n : Symbol(n, Decl(inferTypePredicates.ts, 265, 30)) +>needle : Symbol(needle, Decl(inferTypePredicates.ts, 263, 11)) + diff --git a/tests/baselines/reference/inferTypePredicates.types b/tests/baselines/reference/inferTypePredicates.types index 669933a1c8579..39d0a6937cfe1 100644 --- a/tests/baselines/reference/inferTypePredicates.types +++ b/tests/baselines/reference/inferTypePredicates.types @@ -941,3 +941,22 @@ function narrowFromAny(x: any) { >'number' : "number" } +// test fast path for '===' (which is usually not a type predicate) +declare let needle: string | number | boolean; +>needle : string | number | boolean + +declare let haystack: (string | number | Date | RegExp)[]; +>haystack : (string | number | RegExp | Date)[] + +const finds = haystack.filter(n => n === needle); +>finds : (string | number | RegExp | Date)[] +>haystack.filter(n => n === needle) : (string | number | RegExp | Date)[] +>haystack.filter : { (predicate: (value: string | number | RegExp | Date, index: number, array: (string | number | RegExp | Date)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | number | RegExp | Date, index: number, array: (string | number | RegExp | Date)[]) => unknown, thisArg?: any): (string | number | RegExp | Date)[]; } +>haystack : (string | number | RegExp | Date)[] +>filter : { (predicate: (value: string | number | RegExp | Date, index: number, array: (string | number | RegExp | Date)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | number | RegExp | Date, index: number, array: (string | number | RegExp | Date)[]) => unknown, thisArg?: any): (string | number | RegExp | Date)[]; } +>n => n === needle : (n: string | number | RegExp | Date) => boolean +>n : string | number | RegExp | Date +>n === needle : boolean +>n : string | number | RegExp | Date +>needle : string | number | boolean + diff --git a/tests/cases/compiler/inferTypePredicates.ts b/tests/cases/compiler/inferTypePredicates.ts index 0cf620e194123..3ed30fdb496a7 100644 --- a/tests/cases/compiler/inferTypePredicates.ts +++ b/tests/cases/compiler/inferTypePredicates.ts @@ -260,3 +260,8 @@ function isNumberWithThis(this: Date, x: number | string) { function narrowFromAny(x: any) { return typeof x === 'number'; } + +// test fast path for '===' (which is usually not a type predicate) +declare let needle: string | number | boolean; +declare let haystack: (string | number | Date | RegExp)[]; +const finds = haystack.filter(n => n === needle); From 51a8970d13803904651ec1577c3377ed2eac7ab5 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 6 Mar 2024 09:16:23 -0500 Subject: [PATCH 25/27] make at most two calls to getFTOR for big unions --- src/compiler/checker.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bfb5b4dc3ba86..44296cabd2075 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37464,11 +37464,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const {left, right} = expr; if (isMatchingReference(param.name, left)) { target = right; - } else if (isMatchingReference(param.name, right)) { + } + else if (isMatchingReference(param.name, right)) { target = left; } } if (target) { + // Should this be checkExpressionCached? const targetType = getTypeOfExpression(target); if (!isUnitType(targetType)) { return undefined; @@ -37486,22 +37488,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { flags: FlowFlags.FalseCondition, }; - let isNever: boolean; - if (trueType.flags & TypeFlags.Union) { - isNever = true; - mapType(trueType, t => { - if (!isNever) return neverType; - const falseSubT = getFlowTypeOfReference(param.name, t, t, func, falseCondition); - if (!(falseSubT.flags & TypeFlags.Never)) { - isNever = false; - } - return neverType; - }); + if ((trueType.flags & TypeFlags.Union) && (trueType as UnionType).types.length >= 20) { + const unionType = trueType as UnionType; + const head: UnionType = {...unionType, types: unionType.types.slice(0, 10)}; + const falseSubTypeHead = getFlowTypeOfReference(param.name, head, head, func, falseCondition); + if (!(falseSubTypeHead.flags & TypeFlags.Never)) { + return undefined; + } + const rest: UnionType = {...unionType, types: unionType.types.slice(10)}; + const falseSubTypeRest = getFlowTypeOfReference(param.name, rest, rest, func, falseCondition); + if (!(falseSubTypeRest.flags & TypeFlags.Never)) { + return undefined; + } + return trueType; } else { const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); - isNever = !!(falseSubtype.flags & TypeFlags.Never); + const isNever = !!(falseSubtype.flags & TypeFlags.Never); + return isNever ? trueType : undefined; } - return isNever ? trueType : undefined; } /** From 348509a2c0b2bd8e1fba259acfb56bd16c7bfb1d Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 6 Mar 2024 11:04:25 -0500 Subject: [PATCH 26/27] comments, format --- src/compiler/checker.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 44296cabd2075..2db04b269e5cd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37453,7 +37453,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; if (isBinaryExpression(expr)) { - const {operatorToken} = expr; + // fast path for x => x === y and other similar patterns + // This can only be a type predicate if y is a unit type (e.g. null) but it can be quite expensive to determine that via the usual code path. + const { operatorToken } = expr; const operator = operatorToken.kind; let target; switch (operator) { @@ -37461,7 +37463,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.ExclamationEqualsToken: case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.ExclamationEqualsEqualsToken: - const {left, right} = expr; + const { left, right } = expr; if (isMatchingReference(param.name, left)) { target = right; } @@ -37489,19 +37491,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }; if ((trueType.flags & TypeFlags.Union) && (trueType as UnionType).types.length >= 20) { + // For large unions, try the false check on just the first ten. + // If this is non-never then we can skip checking the remaining constituents. const unionType = trueType as UnionType; - const head: UnionType = {...unionType, types: unionType.types.slice(0, 10)}; + const head: UnionType = { ...unionType, types: unionType.types.slice(0, 10) }; const falseSubTypeHead = getFlowTypeOfReference(param.name, head, head, func, falseCondition); if (!(falseSubTypeHead.flags & TypeFlags.Never)) { return undefined; } - const rest: UnionType = {...unionType, types: unionType.types.slice(10)}; + const rest: UnionType = { ...unionType, types: unionType.types.slice(10) }; const falseSubTypeRest = getFlowTypeOfReference(param.name, rest, rest, func, falseCondition); if (!(falseSubTypeRest.flags & TypeFlags.Never)) { return undefined; } return trueType; - } else { + } + else { const falseSubtype = getFlowTypeOfReference(param.name, trueType, trueType, func, falseCondition); const isNever = !!(falseSubtype.flags & TypeFlags.Never); return isNever ? trueType : undefined; From fd8db07dec1632c1e0ef17c838416cc531481bb5 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 6 Mar 2024 11:10:22 -0500 Subject: [PATCH 27/27] getTypeOfExpression -> checkExpressionCached --- src/compiler/checker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2db04b269e5cd..a3a8a5f8ee7bd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37472,8 +37472,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } if (target) { - // Should this be checkExpressionCached? - const targetType = getTypeOfExpression(target); + const targetType = checkExpressionCached(target); if (!isUnitType(targetType)) { return undefined; }