Search code examples
typescriptgenericstypescript-genericsunion-typesconditional-types

Issue with union types and conditional types


I have following type declarations:

class MyGeneric<T> { }

type ReplaceType<T> = T extends Function ? T : MyGeneric<T> | T;

ReplaceType<T> should resolve to either MyGeneric<T> | T or T depending wheter T it is a function or not:

// Input type:    string
// Expected type: string | MyGeneric<string>
// Actual type:   string | MyGeneric<string>
type Test1 = ReplaceType<string>;

// Input type:    () => void
// Expected type: () => void
// Actual type:   () => void
type Test2 = ReplaceType<() => void>;

Unfortunately, this doesn't work correctly with boolean and union types:

// Input type:    boolean
// Expected type: boolean | MyGeneric<boolean>
// Actual type:   boolean | MyGeneric<true> | MyGeneric<false>
type Test3 = ReplaceType<boolean>;

// Input type:    "foo" | "bar"
// Expected type: "foo" | "bar" | MyGeneric<"foo" | "bar">
// Actual type:   "foo" | "bar" | MyGeneric<"foo"> | MyGeneric<"bar">
type Test4 = ReplaceType<"foo" | "bar">;

Playground link


Solution

  • The reason boolean and unions are have similar behaviors is because the compiler sees boolean as a union of the literal types true and false, so type boolean = true | false (although this definition does not exist explicitly)

    The reason for the behavior is that by design conditional type distribute over a union. This is the designed behavior and allows all sorts of powerful things to be implemented. You can read more on the topic here

    If you don't want conditionals to distribute over the union, you can use the type in a tuple (this will prevent the behavior)

    class MyGeneric<T> { }
    
    type ReplaceType<T> = [T] extends [Function] ? T : MyGeneric<T> | T;
    
    // Input type:    string
    // Expected type: string | MyGeneric<string>
    // Actual type:   string | MyGeneric<string>
    type Test1 = ReplaceType<string>;
    
    // Input type:    () => void
    // Expected type: () => void
    // Actual type:   () => void
    type Test2 = ReplaceType<() => void>;
    
    // Input type:    boolean
    // Expected type: boolean | MyGeneric<boolean>
    // Actual type:   boolean | MyGeneric<boolean>
    type Test3 = ReplaceType<boolean>;
    
    // Input type:    "foo" | "bar"
    // Expected type: "foo" | "bar" | MyGeneric<"foo" | "bar">
    // Actual type:   "foo" | "bar" | MyGeneric<"foo" | "bar">
    type Test4 = ReplaceType<"foo" | "bar">;