Typescript: check if a type is a union
So it seems I've come up with an answer myself!
Here is the type (thanks Titian Cernicova-Dragomir for simplifying it!):
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
type Foo = IsUnion<'abc' | 'def'> // true
type Bar = IsUnion<'abc'> // false
And again UnionToIntersection of jcalz came in handy!
The principle is based on the fact that a union A | B
does not extend an intersection A & B
.
Playground
UPD. I was silly enough to not develop my type from the question into this one, which also works fine:
type IsUnion<T, U extends T = T> =
(T extends any ?
(U extends T ? false : true)
: never) extends false ? false : true
It distributes union T
to constituents, also T
and then checks if U
which is a union extends the constituent T
. If yes, then it's not a union (but I still don't know why it doesn't work without adding extends false ? false : true
, i.e. why the preceding part returns boolean
for unions).
NOTE: This answer was for a case where someone explicitly did not want to use
UnionToIntersection
. That version is simple and easy to understand, so if you have no qualms aboutU2I
, go with that.
I just looked at this again and with the help of @Gerrit0 came up with this:
// Note: Don't pass U explicitly or this will break. If you want, add a helper
// type to avoid that.
type IsUnion<T, U extends T = T> =
T extends unknown ? [U] extends [T] ? false : true : false;
type Test = IsUnion<1 | 2> // true
type Test2 = IsUnion<1> // false
type Test3 = IsUnion<never> // false
Seemed like it could be further simplified and I'm pretty happy with this. The trick here is distributing T
but not U
so that you can compare them. So for type X = 1 | 2
, you end up checking if [1 | 2] extends [1]
which is false, so this type is true
overall. If T = never
we also resolve to false
(thanks Gerrit).
If the type is not a union, then T
and U
are identical, so this type resolves to false
.
Caveats
There are some cases in which this doesn't work. Any union with a member that's assignable to another will resolve to boolean
because of the distribution of T
. Probably the simplest example of this is when {}
is in the union because almost everything (even primitives) are assignable to it. You'll also see it with unions including two object types where one is a subtype of the other, i.e. { x: 1 } | { x: 1, y: 2 }
.
Workarounds
- Use a third
extends
clause (like in Nurbol's answer)
(...) extends false ? false : true;
- Use
never
as the false case:
T extends unknown ? [U] extends [T] ? never : true : never;
- Invert the
extends
at the call site:
true extends IsUnion<T> ? Foo : Bar;
- Since you probably need a conditional type to use this at the call site, wrap it:
type IfUnion<T, Yes, No> = true extends IsUnion<T> ? Yes : No;
There are a lot of other variations that you can do with this type depending on your needs. One idea is to use unknown
for the positive case. Then you can do T & IsUnion<T>
. Or you could just use T
for that and call it AssertUnion
so that the whole type becomes never
if it's not a union. The sky's the limit.
Thanks to @Gerrit0 and @AnyhowStep on gitter for finding my bug & giving feedback on workarounds.