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]