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
.
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).