From bb1dd3955343c84ee2cc2d0840736d7effdc1864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 20 Dec 2024 23:37:19 +0100 Subject: [PATCH] Infer non-narrowing predicates when the contextual signature is a type predicate --- src/compiler/checker.ts | 47 +++-- .../inferContextualTypePredicates1.errors.txt | 36 ++++ .../inferContextualTypePredicates1.symbols | 91 +++++++++ .../inferContextualTypePredicates1.types | 175 ++++++++++++++++++ .../inferContextualTypePredicates1.ts | 20 ++ 5 files changed, 343 insertions(+), 26 deletions(-) create mode 100644 tests/baselines/reference/inferContextualTypePredicates1.errors.txt create mode 100644 tests/baselines/reference/inferContextualTypePredicates1.symbols create mode 100644 tests/baselines/reference/inferContextualTypePredicates1.types create mode 100644 tests/cases/compiler/inferContextualTypePredicates1.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 80b61edd3657a..318e4740c402d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -38778,7 +38778,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } - function getTypePredicateFromBody(func: FunctionLikeDeclaration): TypePredicate | undefined { + function getTypePredicateFromBody(func: FunctionLikeDeclaration, contextualTypePredicate?: IdentifierTypePredicate): TypePredicate | undefined { switch (func.kind) { case SyntaxKind.Constructor: case SyntaxKind.GetAccessor: @@ -38800,41 +38800,35 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { }); if (bailedEarly || !singleReturn || functionHasImplicitReturn(func)) return undefined; } - return checkIfExpressionRefinesAnyParameter(func, singleReturn); - } - - function checkIfExpressionRefinesAnyParameter(func: FunctionLikeDeclaration, expr: Expression): TypePredicate | undefined { - expr = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); + const expr = skipParentheses(singleReturn, /*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) || isRestParameter(param)) { - // 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); - } - }); + return contextualTypePredicate ? + getTypePredicateIfRefinesParameterAtIndex(func, expr, contextualTypePredicate, contextualTypePredicate.parameterIndex) : + forEach(func.parameters, (_, i) => getTypePredicateIfRefinesParameterAtIndex(func, expr, contextualTypePredicate, i)); } - function checkIfExpressionRefinesParameter(func: FunctionLikeDeclaration, expr: Expression, param: ParameterDeclaration, initType: Type): Type | undefined { + function getTypePredicateIfRefinesParameterAtIndex(func: FunctionLikeDeclaration, expr: Expression, contextualTypePredicate: IdentifierTypePredicate | undefined, parameterIndex: number): TypePredicate | undefined { + const param = func.parameters[parameterIndex]; + const initType = getTypeOfSymbol(param.symbol); + if (!initType || initType.flags & TypeFlags.Boolean || !isIdentifier(param.name) || isSymbolAssigned(param.symbol) || isRestParameter(param)) { + // Refining "x: boolean" to "x is true" or "x is false" isn't useful. + return; + } const antecedent = canHaveFlowNode(expr) && expr.flowNode || expr.parent.kind === SyntaxKind.ReturnStatement && (expr.parent as ReturnStatement).flowNode || createFlowNode(FlowFlags.Start, /*node*/ undefined, /*antecedent*/ undefined); const trueCondition = createFlowNode(FlowFlags.TrueCondition, expr, antecedent); const trueType = getFlowTypeOfReference(param.name, initType, initType, func, trueCondition); - if (trueType === initType) return undefined; - + if (!contextualTypePredicate && 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 = createFlowNode(FlowFlags.FalseCondition, expr, antecedent); const falseSubtype = getFlowTypeOfReference(param.name, initType, trueType, func, falseCondition); - return falseSubtype.flags & TypeFlags.Never ? trueType : undefined; + return falseSubtype.flags & TypeFlags.Never ? createTypePredicate(TypePredicateKind.Identifier, unescapeLeadingUnderscores(param.name.escapedText), parameterIndex, trueType) : undefined; } /** @@ -38978,10 +38972,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { inferFromAnnotatedParameters(signature, contextualSignature, inferenceContext!); } } - if (contextualSignature && !getReturnTypeFromAnnotation(node) && !signature.resolvedReturnType) { - const returnType = getReturnTypeFromBody(node, checkMode); - if (!signature.resolvedReturnType) { - signature.resolvedReturnType = returnType; + if (contextualSignature && !getReturnTypeFromAnnotation(node)) { + const returnType = signature.resolvedReturnType ?? getReturnTypeFromBody(node, checkMode); + signature.resolvedReturnType ??= returnType; + if (signature.resolvedReturnType.flags && TypeFlags.BooleanLike && contextualSignature.resolvedTypePredicate && contextualSignature.resolvedTypePredicate !== noTypePredicate && contextualSignature.resolvedTypePredicate.kind === TypePredicateKind.Identifier) { + signature.resolvedTypePredicate ??= getTypePredicateFromBody(node, contextualSignature.resolvedTypePredicate) ?? noTypePredicate; } } checkSignatureDeclaration(node); diff --git a/tests/baselines/reference/inferContextualTypePredicates1.errors.txt b/tests/baselines/reference/inferContextualTypePredicates1.errors.txt new file mode 100644 index 0000000000000..566c2f4315e97 --- /dev/null +++ b/tests/baselines/reference/inferContextualTypePredicates1.errors.txt @@ -0,0 +1,36 @@ +inferContextualTypePredicates1.ts(13,26): error TS2345: Argument of type '(item: Foo | Bar) => false' is not assignable to parameter of type '(a: Foo | Bar) => a is Foo | Bar'. + Signature '(item: Foo | Bar): false' must be a type predicate. +inferContextualTypePredicates1.ts(14,26): error TS2345: Argument of type '(item: Foo | Bar) => true' is not assignable to parameter of type '(a: Foo | Bar) => a is Foo | Bar'. + Signature '(item: Foo | Bar): true' must be a type predicate. +inferContextualTypePredicates1.ts(17,7): error TS2322: Type '(a: string | null, b: string | null) => boolean' is not assignable to type '(a: string | null, b: string | null) => b is string'. + Signature '(a: string | null, b: string | null): boolean' must be a type predicate. + + +==== inferContextualTypePredicates1.ts (3 errors) ==== + type Foo = { type: "foo"; foo: number }; + type Bar = { type: "bar"; bar: string }; + + declare function skipIf( + as: A[], + predicate: (a: A) => a is B, + ): Exclude[]; + + declare const items: (Foo | Bar)[]; + + const r1 = skipIf(items, (item) => item.type === "foo"); // ok + const r2 = skipIf(items, (item) => item.type === "foo" || item.type === "bar"); // ok + const r3 = skipIf(items, (item) => false); // error + ~~~~~~~~~~~~~~~ +!!! error TS2345: Argument of type '(item: Foo | Bar) => false' is not assignable to parameter of type '(a: Foo | Bar) => a is Foo | Bar'. +!!! error TS2345: Signature '(item: Foo | Bar): false' must be a type predicate. + const r4 = skipIf(items, (item) => true); // error + ~~~~~~~~~~~~~~ +!!! error TS2345: Argument of type '(item: Foo | Bar) => true' is not assignable to parameter of type '(a: Foo | Bar) => a is Foo | Bar'. +!!! error TS2345: Signature '(item: Foo | Bar): true' must be a type predicate. + + const pred1: (a: string | null, b: string | null) => b is string = (a, b) => typeof b === 'string'; // ok + const pred2: (a: string | null, b: string | null) => b is string = (a, b) => typeof a === 'string'; // error + ~~~~~ +!!! error TS2322: Type '(a: string | null, b: string | null) => boolean' is not assignable to type '(a: string | null, b: string | null) => b is string'. +!!! error TS2322: Signature '(a: string | null, b: string | null): boolean' must be a type predicate. + \ No newline at end of file diff --git a/tests/baselines/reference/inferContextualTypePredicates1.symbols b/tests/baselines/reference/inferContextualTypePredicates1.symbols new file mode 100644 index 0000000000000..93bc796283858 --- /dev/null +++ b/tests/baselines/reference/inferContextualTypePredicates1.symbols @@ -0,0 +1,91 @@ +//// [tests/cases/compiler/inferContextualTypePredicates1.ts] //// + +=== inferContextualTypePredicates1.ts === +type Foo = { type: "foo"; foo: number }; +>Foo : Symbol(Foo, Decl(inferContextualTypePredicates1.ts, 0, 0)) +>type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 0, 12)) +>foo : Symbol(foo, Decl(inferContextualTypePredicates1.ts, 0, 25)) + +type Bar = { type: "bar"; bar: string }; +>Bar : Symbol(Bar, Decl(inferContextualTypePredicates1.ts, 0, 40)) +>type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 1, 12)) +>bar : Symbol(bar, Decl(inferContextualTypePredicates1.ts, 1, 25)) + +declare function skipIf( +>skipIf : Symbol(skipIf, Decl(inferContextualTypePredicates1.ts, 1, 40)) +>A : Symbol(A, Decl(inferContextualTypePredicates1.ts, 3, 24)) +>B : Symbol(B, Decl(inferContextualTypePredicates1.ts, 3, 26)) +>A : Symbol(A, Decl(inferContextualTypePredicates1.ts, 3, 24)) + + as: A[], +>as : Symbol(as, Decl(inferContextualTypePredicates1.ts, 3, 40)) +>A : Symbol(A, Decl(inferContextualTypePredicates1.ts, 3, 24)) + + predicate: (a: A) => a is B, +>predicate : Symbol(predicate, Decl(inferContextualTypePredicates1.ts, 4, 10)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 5, 14)) +>A : Symbol(A, Decl(inferContextualTypePredicates1.ts, 3, 24)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 5, 14)) +>B : Symbol(B, Decl(inferContextualTypePredicates1.ts, 3, 26)) + +): Exclude[]; +>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --)) +>A : Symbol(A, Decl(inferContextualTypePredicates1.ts, 3, 24)) +>B : Symbol(B, Decl(inferContextualTypePredicates1.ts, 3, 26)) + +declare const items: (Foo | Bar)[]; +>items : Symbol(items, Decl(inferContextualTypePredicates1.ts, 8, 13)) +>Foo : Symbol(Foo, Decl(inferContextualTypePredicates1.ts, 0, 0)) +>Bar : Symbol(Bar, Decl(inferContextualTypePredicates1.ts, 0, 40)) + +const r1 = skipIf(items, (item) => item.type === "foo"); // ok +>r1 : Symbol(r1, Decl(inferContextualTypePredicates1.ts, 10, 5)) +>skipIf : Symbol(skipIf, Decl(inferContextualTypePredicates1.ts, 1, 40)) +>items : Symbol(items, Decl(inferContextualTypePredicates1.ts, 8, 13)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 10, 26)) +>item.type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 0, 12), Decl(inferContextualTypePredicates1.ts, 1, 12)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 10, 26)) +>type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 0, 12), Decl(inferContextualTypePredicates1.ts, 1, 12)) + +const r2 = skipIf(items, (item) => item.type === "foo" || item.type === "bar"); // ok +>r2 : Symbol(r2, Decl(inferContextualTypePredicates1.ts, 11, 5)) +>skipIf : Symbol(skipIf, Decl(inferContextualTypePredicates1.ts, 1, 40)) +>items : Symbol(items, Decl(inferContextualTypePredicates1.ts, 8, 13)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 11, 26)) +>item.type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 0, 12), Decl(inferContextualTypePredicates1.ts, 1, 12)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 11, 26)) +>type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 0, 12), Decl(inferContextualTypePredicates1.ts, 1, 12)) +>item.type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 1, 12)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 11, 26)) +>type : Symbol(type, Decl(inferContextualTypePredicates1.ts, 1, 12)) + +const r3 = skipIf(items, (item) => false); // error +>r3 : Symbol(r3, Decl(inferContextualTypePredicates1.ts, 12, 5)) +>skipIf : Symbol(skipIf, Decl(inferContextualTypePredicates1.ts, 1, 40)) +>items : Symbol(items, Decl(inferContextualTypePredicates1.ts, 8, 13)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 12, 26)) + +const r4 = skipIf(items, (item) => true); // error +>r4 : Symbol(r4, Decl(inferContextualTypePredicates1.ts, 13, 5)) +>skipIf : Symbol(skipIf, Decl(inferContextualTypePredicates1.ts, 1, 40)) +>items : Symbol(items, Decl(inferContextualTypePredicates1.ts, 8, 13)) +>item : Symbol(item, Decl(inferContextualTypePredicates1.ts, 13, 26)) + +const pred1: (a: string | null, b: string | null) => b is string = (a, b) => typeof b === 'string'; // ok +>pred1 : Symbol(pred1, Decl(inferContextualTypePredicates1.ts, 15, 5)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 15, 14)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 15, 31)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 15, 31)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 15, 68)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 15, 70)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 15, 70)) + +const pred2: (a: string | null, b: string | null) => b is string = (a, b) => typeof a === 'string'; // error +>pred2 : Symbol(pred2, Decl(inferContextualTypePredicates1.ts, 16, 5)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 16, 14)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 16, 31)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 16, 31)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 16, 68)) +>b : Symbol(b, Decl(inferContextualTypePredicates1.ts, 16, 70)) +>a : Symbol(a, Decl(inferContextualTypePredicates1.ts, 16, 68)) + diff --git a/tests/baselines/reference/inferContextualTypePredicates1.types b/tests/baselines/reference/inferContextualTypePredicates1.types new file mode 100644 index 0000000000000..c3d0c71e7de0c --- /dev/null +++ b/tests/baselines/reference/inferContextualTypePredicates1.types @@ -0,0 +1,175 @@ +//// [tests/cases/compiler/inferContextualTypePredicates1.ts] //// + +=== inferContextualTypePredicates1.ts === +type Foo = { type: "foo"; foo: number }; +>Foo : Foo +> : ^^^ +>type : "foo" +> : ^^^^^ +>foo : number +> : ^^^^^^ + +type Bar = { type: "bar"; bar: string }; +>Bar : Bar +> : ^^^ +>type : "bar" +> : ^^^^^ +>bar : string +> : ^^^^^^ + +declare function skipIf( +>skipIf : (as: A[], predicate: (a: A) => a is B) => Exclude[] +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^ + + as: A[], +>as : A[] +> : ^^^ + + predicate: (a: A) => a is B, +>predicate : (a: A) => a is B +> : ^ ^^ ^^^^^ +>a : A +> : ^ + +): Exclude[]; + +declare const items: (Foo | Bar)[]; +>items : (Foo | Bar)[] +> : ^^^^^^^^^^^^^ + +const r1 = skipIf(items, (item) => item.type === "foo"); // ok +>r1 : Bar[] +> : ^^^^^ +>skipIf(items, (item) => item.type === "foo") : Bar[] +> : ^^^^^ +>skipIf : (as: A[], predicate: (a: A) => a is B) => Exclude[] +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^ +>items : (Foo | Bar)[] +> : ^^^^^^^^^^^^^ +>(item) => item.type === "foo" : (item: Foo | Bar) => item is Foo +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>item.type === "foo" : boolean +> : ^^^^^^^ +>item.type : "foo" | "bar" +> : ^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>type : "foo" | "bar" +> : ^^^^^^^^^^^^^ +>"foo" : "foo" +> : ^^^^^ + +const r2 = skipIf(items, (item) => item.type === "foo" || item.type === "bar"); // ok +>r2 : never[] +> : ^^^^^^^ +>skipIf(items, (item) => item.type === "foo" || item.type === "bar") : never[] +> : ^^^^^^^ +>skipIf : (as: A[], predicate: (a: A) => a is B) => Exclude[] +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^ +>items : (Foo | Bar)[] +> : ^^^^^^^^^^^^^ +>(item) => item.type === "foo" || item.type === "bar" : (item: Foo | Bar) => item is Foo | Bar +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>item.type === "foo" || item.type === "bar" : boolean +> : ^^^^^^^ +>item.type === "foo" : boolean +> : ^^^^^^^ +>item.type : "foo" | "bar" +> : ^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>type : "foo" | "bar" +> : ^^^^^^^^^^^^^ +>"foo" : "foo" +> : ^^^^^ +>item.type === "bar" : boolean +> : ^^^^^^^ +>item.type : "bar" +> : ^^^^^ +>item : Bar +> : ^^^ +>type : "bar" +> : ^^^^^ +>"bar" : "bar" +> : ^^^^^ + +const r3 = skipIf(items, (item) => false); // error +>r3 : never[] +> : ^^^^^^^ +>skipIf(items, (item) => false) : never[] +> : ^^^^^^^ +>skipIf : (as: A[], predicate: (a: A) => a is B) => Exclude[] +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^ +>items : (Foo | Bar)[] +> : ^^^^^^^^^^^^^ +>(item) => false : (item: Foo | Bar) => false +> : ^ ^^^^^^^^^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>false : false +> : ^^^^^ + +const r4 = skipIf(items, (item) => true); // error +>r4 : never[] +> : ^^^^^^^ +>skipIf(items, (item) => true) : never[] +> : ^^^^^^^ +>skipIf : (as: A[], predicate: (a: A) => a is B) => Exclude[] +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^ +>items : (Foo | Bar)[] +> : ^^^^^^^^^^^^^ +>(item) => true : (item: Foo | Bar) => true +> : ^ ^^^^^^^^^^^^^^^^^^^^ +>item : Foo | Bar +> : ^^^^^^^^^ +>true : true +> : ^^^^ + +const pred1: (a: string | null, b: string | null) => b is string = (a, b) => typeof b === 'string'; // ok +>pred1 : (a: string | null, b: string | null) => b is string +> : ^ ^^ ^^ ^^ ^^^^^ +>a : string | null +> : ^^^^^^^^^^^^^ +>b : string | null +> : ^^^^^^^^^^^^^ +>(a, b) => typeof b === 'string' : (a: string | null, b: string | null) => b is string +> : ^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : string | null +> : ^^^^^^^^^^^^^ +>b : string | null +> : ^^^^^^^^^^^^^ +>typeof b === 'string' : boolean +> : ^^^^^^^ +>typeof b : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>b : string | null +> : ^^^^^^^^^^^^^ +>'string' : "string" +> : ^^^^^^^^ + +const pred2: (a: string | null, b: string | null) => b is string = (a, b) => typeof a === 'string'; // error +>pred2 : (a: string | null, b: string | null) => b is string +> : ^ ^^ ^^ ^^ ^^^^^ +>a : string | null +> : ^^^^^^^^^^^^^ +>b : string | null +> : ^^^^^^^^^^^^^ +>(a, b) => typeof a === 'string' : (a: string | null, b: string | null) => boolean +> : ^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : string | null +> : ^^^^^^^^^^^^^ +>b : string | null +> : ^^^^^^^^^^^^^ +>typeof a === 'string' : boolean +> : ^^^^^^^ +>typeof a : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : string | null +> : ^^^^^^^^^^^^^ +>'string' : "string" +> : ^^^^^^^^ + diff --git a/tests/cases/compiler/inferContextualTypePredicates1.ts b/tests/cases/compiler/inferContextualTypePredicates1.ts new file mode 100644 index 0000000000000..4948fbe00f6b3 --- /dev/null +++ b/tests/cases/compiler/inferContextualTypePredicates1.ts @@ -0,0 +1,20 @@ +// @strict: true +// @noEmit: true + +type Foo = { type: "foo"; foo: number }; +type Bar = { type: "bar"; bar: string }; + +declare function skipIf( + as: A[], + predicate: (a: A) => a is B, +): Exclude[]; + +declare const items: (Foo | Bar)[]; + +const r1 = skipIf(items, (item) => item.type === "foo"); // ok +const r2 = skipIf(items, (item) => item.type === "foo" || item.type === "bar"); // ok +const r3 = skipIf(items, (item) => false); // error +const r4 = skipIf(items, (item) => true); // error + +const pred1: (a: string | null, b: string | null) => b is string = (a, b) => typeof b === 'string'; // ok +const pred2: (a: string | null, b: string | null) => b is string = (a, b) => typeof a === 'string'; // error