Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to prevent union types in TypeScript?

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.

like image 891
Daniel Birowsky Popeski Avatar asked Jun 01 '18 08:06

Daniel Birowsky Popeski


3 Answers

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

like image 68
Titian Cernicova-Dragomir Avatar answered Sep 19 '22 07:09

Titian Cernicova-Dragomir


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.

like image 33
jcalz Avatar answered Sep 21 '22 07:09

jcalz


This also works:

type NoUnion<T, U = T> = T extends U ? [U] extends [T] ? T : never : never;
like image 32
JohnLock Avatar answered Sep 22 '22 07:09

JohnLock