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 about U2I, 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.