Typescript using a union as a generic argument wrapped in a conditional

Viewed 107

Take the following deliberately simplified code example (my real use case is more complex, but the problem boils down to this):

type TArbitraryUnion = "a" | "b";

type TObjectWithGeneric<T> = {
    prop: T;
};

type TTypeWithConditional<T> = T extends string
    ? TObjectWithGeneric<T>
    : never;

type TTypeWithoutConditional<T> = TObjectWithGeneric<T>;

type TType1 = TTypeWithConditional<TArbitraryUnion>; // TObjectWithGeneric<"a"> | TObjectWithGeneric<"b">
type TType2 = TTypeWithoutConditional<TArbitraryUnion>; // {prop: TArbitraryUnion;}

TType1 ends up becoming a union of generics, whereas TType2 becomes a generic of the union.

Is there any way of creating the definition of TTypeWithConditional such that it becomes TObjectWithGeneric<"a" | "b"> instead of TObjectWithGeneric<"a"> | TObjectWithGeneric<"b">?

I'm struggling to understand why TType1 and TType2 are ending up as different types. Any help appreciated.

1 Answers

When using conditional types with generics, the condition acts distributively on union types. See the documentation on Distributive Conditional Types.

As another example of how the distribution works, and which shows why it is important:

Suppose we modify TArbitraryUnion to be the union type "a" | "b" | 1. When we do this, TType1 becomes TObjectWithGeneric<"a"> | TObjectWithGeneric<"b"> (i.e. {prop: "a" | "b"}), and TType2 remains {prop: TArbitraryUnion} (i.e. {prop: "a" | "b" | 1}). Since TType2 does not use a conditional type, the condition does not need to be distributed among the type parameter, but it does need to be distributed among TType1 because 1 extends string falls into the false case (never).

Related