typescripttypescript-genericsunion-typesconditional-types

Why union-string generic type is treated as a specific literal when passing to a conditional type?


Typescript version: 5.6.2

Why the ApprovalType was tampered into specific values of ApprovalType if the function parameter type is a conditional type, even if I explicitly pass in the generic parameter <ApprovalType> when calling the function?

Playground

type ApprovalType = "PENDING" | "APPROVED" | "REJECTED";

type A<T> = {
    options: T[];
};

type B<T> = {
    options: T[];
};

function conditionalTypeFn<T>(props: T extends string ? B<T> : A<T>) {
    return props;
}
conditionalTypeFn<ApprovalType>({
    /**
     * ❌ Type '("PENDING" | "APPROVED" | "REJECTED")[]' is not assignable
     * to type '"PENDING"[] | "APPROVED"[] | "REJECTED"[]'
     */
    options: ["PENDING", "APPROVED", "REJECTED"],
});

function unionTypeFn<T>(props: A<T> | B<T>) {
    return props;
}
unionTypeFn<ApprovalType>({
    /* ✅ no error */
    options: ["PENDING", "APPROVED", "REJECTED"],
});

Solution

  • By default, as explained in the documentation, conditional types behave in a distributive manner when used on a union type.

    Here is a simplified example that demonstrates this:

    type ApprovalType = "PENDING" | "APPROVED" | "REJECTED";
    
    type Distributed<T> = T extends string ? T[] : never;
    
    type ApprovalTypes = Distributed<ApprovalType>;
    // type ApprovalTypes = "PENDING"[] | "APPROVED"[] | "REJECTED"[]
    

    To avoid the distributive behavior, you can use square brackets in your conditional type. Applied to the same example, this would yield:

    type ApprovalType = "PENDING" | "APPROVED" | "REJECTED";
    
    type NonDistributed<T> = [T] extends [string] ? T[] : never;
    
    type ApprovalTypes = NonDistributed<ApprovalType>;
    // type ApprovalTypes = ("PENDING" | "APPROVED" | "REJECTED")[]
    

    Or, applied to the code in your question:

    function conditionalTypeFn<T>(props: [T] extends [string] ? B<T> : A<T>) {
        return props;
    }
    

    Playground link