9

I'm new to conditional types, so I tried the most obvious static way, no success:

type NoUnion<Key> =
  Key extends 'a' ? 'a' :
  Key extends 'b' ? 'b' :
  never;

type B = NoUnion<'a'|'b'>;

The B type is still a union. Would somebody please school me?

Here's a playground.

Daniel Birowsky Popeski
  • 8,752
  • 12
  • 60
  • 125

3 Answers3

21

I am unsure what the usecase for this is, but we can force the NoUnion to never if the passed type is a union type.

As other mentioned conditional types distribute over a union, this is called distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

The key there is 'naked type', if we wrap the type in a tuple type for example the conditional type will no longer be distributive.

type UnionToIntersection<U> = 
    (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never 

type NoUnion<Key> =
    // If this is a simple type UnionToIntersection<Key> will be the same type, otherwise it will an intersection of all types in the union and probably will not extend `Key`
    [Key] extends [UnionToIntersection<Key>] ? Key : never; 

type A = NoUnion<'a'|'b'>; // never
type B = NoUnion<'a'>; // a
type OtherUnion = NoUnion<string | number>; // never
type OtherType = NoUnion<number>; // number
type OtherBoolean = NoUnion<boolean>; // never since boolean is just true|false

The last example is an issue, since boolean is seen by the compiler as true|false, NoUnion<boolean> will actually be never. Without more details of what exactly you are trying to achieve it is difficult to know if this is a deal breaker, but it could be solved by treating boolean as a special case:

type NoUnion<Key> =
    [Key] extends [boolean] ? boolean :
    [Key] extends [UnionToIntersection<Key>] ? Key : never;

Note: UnionToIntersection is taken from here

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 2
    @Birowsky :)) There are no upvotes in writing docs :P. This is more fun ;) – Titian Cernicova-Dragomir Jun 01 '18 at 10:34
  • So.. I tried implementing this towards my problem, but all I get is a broken TS :/ If you could [take a look here](http://jsbin.com/yebuner/1/edit?js), I'd be sooo grateful! The goal is to fix the problem within the resolver without assertion. (It's a jsbin because the playground link was too big for comment) – Daniel Birowsky Popeski Jun 01 '18 at 11:44
  • 1
    @Birowsky looking into it – Titian Cernicova-Dragomir Jun 01 '18 at 11:48
  • @Birowsky Maybe this would be workable: `function resolver(a: Command): NoUnion extends never ? never : CommandToHandler[cmdName]['out'] { return handlers[a.kind](a); } let c : Command<"a"> resolver(c) // ok let cu : Command<"a" | "b"> resolver(cu) // returns never ` – Titian Cernicova-Dragomir Jun 01 '18 at 11:56
  • Sorry mate, but it doesn't fix the `Error:(26, 10) TS2349: Cannot invoke an expression whose type lacks a call signature. Type '((arg: Command<"a">) => number) | ((arg: Command<"b">) => string)' has no compatible call signatures.` You should see this problem if you paste the snippet from jsbin to TS playground. Thanx, and sorry for not being explicit. – Daniel Birowsky Popeski Jun 01 '18 at 12:07
  • @Birowsky yeah .. inside the function the union will still be there no way I know of to get around that … the call will return never if it's invoked with a union .. – Titian Cernicova-Dragomir Jun 01 '18 at 12:35
6

By the way, the "simpler" one I was trying to come up with looks like this:

( NOTE: the following doesn't work in TS after v3.3, due to microsoft/TypeScript#34504:

// type NotAUnion<T> = [T] extends [infer U] ? 
//  U extends any ? [T] extends [U] ? T : never : never : never;

instead one can use the following since defaults still get instantiated before distribution, at least for now: )

type NotAUnion<T, U = T> =
  U extends any ? [T] extends [U] ? T : never : never;

This should work (please test it; not sure why I got the original version in my answer to another question wrong but it's fixed now ). It's a similar idea to the UnionToIntersection: you want to make sure that a type T is assignable to each part of T if you distribute it. In general that's only true if T is a union with just one constituent part (which is also called "not a union").

Anyway, @TitianCernicovaDragomir's answer is perfectly fine also. Just wanted to get this version out there. Cheers.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Cheers monsieur! By the way, I thought the brackets in `[T]` were only used for tuples. But here it seems to have some other semantics. Could you clarify please? (A link to a resource is perfectly fine) – Daniel Birowsky Popeski Jun 01 '18 at 13:57
  • Yeah, it *is* used as a tuple, in a hacky way meant to prevent distributing. (See @TitianCD's comment about "naked type" above). Let's see if I can find a link. – jcalz Jun 01 '18 at 14:19
  • 1
    [This issue](https://github.com/Microsoft/TypeScript/issues/21870) is the closest I can find, with not much explanation. `T extends U ? X : Y` distributes `T` if `T` is a "naked type parameter". The idea is to "clothe" it: `[T] extends [U]` should hold if and only if `T extends U` (since single-element tuples are covariant in their type) but prevents the distribution of `T`. There's nothing special about using tuple syntax here; any covariant type will do: `{x: T} extends {x: U}` holds if and only if `T extends U` hold. But `[T]` is probably the shortest way to do it. – jcalz Jun 01 '18 at 14:44
  • 1
    That is a neat solution. But somehow, it doesn't work in versions > `3.3.3` (last version I could test in the playground). You can compare [`3.3.3`](https://tsplay.dev/DmMezm) with [`3.5.1`](https://tsplay.dev/Lw6JGw). It seems like an inferred type parameter `U` cannot be made "naked" anymore to create distributed union type - sounds like a bug to me. – ford04 Nov 18 '20 at 09:57
  • 1
    @ford04 Thank you for pointing that out. I see [microsoft/TypeScript#34504](https://github.com/microsoft/TypeScript/issues/34504) mentions this, so I guess I have to find and change any answers of mine that relied on the prior behavior (not that such things are easy to search for) – jcalz Nov 18 '20 at 14:43
2

This also works:

type NoUnion<T, U = T> = T extends U ? [U] extends [T] ? T : never : never;
JohnLock
  • 381
  • 3
  • 5