Home > OS >  How can you remove duplicate types and avoid ending up with never?
How can you remove duplicate types and avoid ending up with never?

Time:11-06

Background

I have an interface that takes 2 type parameters. The second parameter is optional and defaults to void. I also have a utility type (CommandReturnType) that uses a conditional type to prevent void from being a return type if the second type parameter is not supplied.

interface Command<IOType, AdditionalOutputType = void> {
    execute(input: IOType): CommandReturnType<IOType, AdditionalOutputType>;
}

type CommandReturnType<A, B> = B extends void ? A : A | B;

I created a utility type that uses infer to return a tuple containing the 2 type parameter types.

type GetCommandParamTypes<T extends Command<unknown, unknown>> =
    T extends Command<infer IO, infer A> ?
    [IO, A] :
    never;

It works as expected if i supply it with the interface with only one type parameter - it correctly returns void as the second parameter.

type type1 = GetCommandParamTypes<Command<string>>; // [string, void]

But if i supply it with a class that implements the interface with only one type parameter, it returns a tuple with that type in both slots.

class TypeACommand implements Command<string> {

    execute(input: string): string {
        return input;
    }
}

type type2 = GetCommandParamTypes<TypeACommand>; // [string, string]

Attempted solution

As with the majority of questions i intend to post on SO, as i write out the question, it helps clarify what's actually going on.

I (think i) now understand that the input type and return type of the execute function are retrieved when examining the class. So it occurred to me that i could use Exclude<> to remove any duplicates from the second type.

 type GetCommandParamTypes<T extends Command<unknown, unknown>> =
    T extends Command<infer IO, infer A> ?
    [IO, Exclude<A, IO>] :
    never;

Which works well when a type is supplied for the second type parameter:

class MixedTypeCommand implements Command<string | number, Function> {

    execute(input: string | number): string | number | Function {
       // do something with input here...
       return something;
    }
}

type type3 = GetCommandParamTypes<MixedTypeCommand>; // [string | number, Function] instead of [string | number, string | number | Function]

But Exclude<type type> returns never when it's type parameters match.

type type4 = GetCommandParamTypes<TypeACommand>; // [string, never]

So i came up with a not so cunning plan to just replace never with void. Which failed.

type ReplaceNever<T> = T extends never ? void : T;

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, ReplaceNever<Exclude<A, IO>>] :
    never;

type type5 = GetCommandParamTypes<TypeACommand>; // [string, never]

Question

Is there a simple way to replace never?

And if not, is there another solution that would enable me to remove the duplicate types from the second tuple slot and if the result of that is nothing, replace that with void?

So:

 type wrong = GetCommandParamTypes<TypeACommand>; // [string, string]

would become:

 type right = GetCommandParamTypes<TypeACommand>; // [string, void]

CodePudding user response:

I found the solution in this SO answer

Conditional types distribute over naked type parameters. This means that the conditional type gets applied to each member of the union. never is seen as the empty union. So the conditional type never gets applied (since there are no members in the union to apply it to) resulting in the never type.

The simple solution is to disable the distributive behavior of conditional types using a tuple:

So simply changing

type ReplaceNever<T> = T extends never ? void : T;

to

type ReplaceNever<T> = [T] extends [never] ? void : T;

solved my problem.

  • Related