4

TypeScript seems to have problems to infer union types of type guards. As an example, consider a function to combine an array of type guards with the following signature

function combine<T>(guards: ((x: any) => x is T)[]): (x: any) => x is T

and consider the following type guards with A and B having different properties

function isA(x: any): x is A
function isB(x: any): x is B

Now I would expect combine([isA, isB]) to work and have the inferred type (x: any) => x is A | B but instead I get an error saying that an argument of type((x: any) => x is A | (x: any) => x is B)[] is not assignable to parameter of type (x: any) => x is A, meaning that T is inferred as A rather than A | B.

When specifying T explicitely, i.e. combine<A|B>([isA, isB]), it works as expected. Is there a way to change the signature of combine such that this could be inferred?

ipsec
  • 680
  • 4
  • 11
  • You can always use `function combine2(guards: [((x: any) => x is T), ((x: any) => x is U)]): (x: any) => x is T | U`, but I guess you want to accept an array with various amount of items? – Caramiriel Sep 08 '18 at 16:37
  • 1
    Exactly, I am looking for a solution which works with any (positive) number of array elements. – ipsec Sep 08 '18 at 16:46

1 Answers1

7

You can use a type parameter to denote the whole function instead of just the guarded type. This allows the compiler to infer a union of guard functions. We can then use a conditional type to extract the union of guarded types :

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

declare function combine<T extends ((x: any) => x is any)>(guards: T[]): (x: any) => x is GuardType<T>

let isAB = combine([isA, isB]); // (x:any) => x is A|B
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • I've been trying to solve this one for long enough, you just saved me <3 Maybe you could help this guy person too https://stackoverflow.com/questions/52236191/how-to-get-a-function-type-that-omitted-last-argument I've submitted an answer but it's rather hacky and I would like to know if there is a better solution. – Clément Prévost Sep 08 '18 at 18:47
  • @ClémentPrévost I saw your answer, I was the one to upvote it :) – Titian Cernicova-Dragomir Sep 08 '18 at 19:08
  • 1
    This is amazing. I was wondering how to do this with an intersection type, to achieve this: `let isAB = combine([isA, isB]); // (x:any) => x is A & B` – blid Nov 07 '19 at 10:18
  • 1
    @blid I think you could do it the same, `UnionToIntersection` is the only extra thing you need https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type – Titian Cernicova-Dragomir Nov 07 '19 at 11:25
  • @TitianCernicova-Dragomir Thank you, it worked like a charm: https://stackblitz.com/edit/typescript-k4wvjb. – blid Nov 07 '19 at 12:00
  • I'm trying to do this but accept an array of type guard _or_ predicate functions, with the result hopefully being a return type of `boolean & x is A | B`. I want to use this as an argument to e.g. `Array.filter` so I get type resolution as well as the conditions tested by the predicates. However using any predicate in the argument list makes it so only the first type guard is used as return type for the whole `combine`. I'll start a new question – Ran Lottem Mar 20 '20 at 21:36