2

I've followed the example explained here to create an Or function which takes a number of type guards and returns a type guard for the union type. e.g.: x is A + x is B => x is A | B. However I'm not able to use the returned function as an argument to an Array.filter.

Definitions:

type TypeGuard<A, B extends A> = (a: A) => a is B;
type GuardType<T> = T extends (o: any) => o is infer U ? U : never

class A { q: any }
class B { p: any }
declare function isA(x: any): x is A
declare function isB(x: any): x is B

function Or<T extends TypeGuard<any, any>>(guards: T[]): T extends TypeGuard<infer A, any> ? (a: A) => a is GuardType<T> : never;
function Or<T extends TypeGuard<any, any>>(guards: T[]) {
    return function (arg: T) {
        return guards.some(function (predicate) {
            predicate(arg);
        });
    }
}

Code example:

let isAOrB = Or([isA, isB]); // inferred as ((a: any) => a is A) | ((a: any) => a is B)
let isOr_value = isAOrB({ q: 'a' }); // here isAOrB is inferred as (a: any) => a is A | B which is what I want
[{}].filter(isAOrB).forEach(x => { }); // here I expected x's type to be inferred as A | B because of the type guard, however filter's overload is the regular one, returning the same {}[] type as the source array

I know I can explicitly write a lambda expression as the filter argument to force the type inference:

[{}].filter((x): x is A | B => isAOrB(x)).forEach(x => { });

But this is exactly what I'd like to avoid.

The same problem with an And function combining type guards

I've used the UnionToIntersection construct shown here but cannot correctly type the And function, here's my attempt and the error I'm getting:

function And<T extends TypeGuard<any, any>>(guards: T[]): T extends TypeGuard<infer A, any> ? (a: A) => a is UnionToIntersection<GuardType<T>> : never;
// the above gives error A type predicate's type must be assignable to its parameter's type.
  Type 'UnionToIntersection<GuardType<T>>' is not assignable to type 'A'.
    Type 'unknown' is not assignable to type 'A'.
Ran Lottem
  • 476
  • 5
  • 17

1 Answers1

3

The problem here seems to be that you are accidentally distributing your conditional type. If T is a bare type parameter, then the conditional type T extends Foo ? Bar : Baz will end up breaking apart T into its union members, evaluating the conditional for each member, and unioning the results back together. This gives you the undesirable ((a: any) => a is A) | ((a: any) => a is B).

The easiest way to turn off such distribution is to "clothe" the bare type parameter in a single-element tuple, like [T] extends [Foo] ? Bar : Baz:

function Or<T extends TypeGuard<any, any>>(guards: T[]): 
  [T] extends [TypeGuard<infer A, any>] ? (a: A) => a is GuardType<T> : never;

That should give you the behavior you are looking for:

[{}].filter(isAOrB).forEach(x => { }); // x is A | B

The same thing applies to your And, with the minor wrinkle that the compiler will not understand that UnionToIntersection<...> will be a valid narrowing for the type guard function, so you might want to wrap it in Extract<> like this:

declare function And<T extends TypeGuard<any, any>>(guards: T[]):
    [T] extends [TypeGuard<infer A, any>] ? 
    (a: A) => a is Extract<UnionToIntersection<GuardType<T>>, A> : never;

let isAAndB = And([isA, isB]); 
let isAnd_value = isAAndB({ q: 'a' }); 
[{}].filter(isAAndB).forEach(x => { }); // A & B

Looks good to me now. Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360