Búsqueda de sitios web

typescript - ¿Cómo puedes eliminar tipos duplicados y evitar terminar con never?


Antecedentes

Tengo una interfaz que toma 2 parámetros de tipo. El segundo parámetro es opcional y por defecto es void. También tengo un tipo de utilidad (CommandReturnType) que utiliza un tipo condicional para evitar que void sea un tipo de retorno si el segundo parámetro de tipo no se suministra.

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

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

Creé un tipo de utilidad que usa infer para devolver una tupla que contiene los 2 tipos de parámetros de tipo.

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

Funciona como se espera si le proporciono la interfaz con solo un parámetro de tipo: devuelve correctamente void como el segundo parámetro.

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

Pero si lo proporciono con una clase que implementa la interfaz con solo un parámetro de tipo, devuelve una tupla con ese tipo en ambos espacios.

class TypeACommand implements Command<string> {

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

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

Solución intentada

Como con la mayoría de las preguntas que tengo la intención de publicar en SO, al escribir la pregunta, ayuda a aclarar lo que realmente está ocurriendo.

Ahora (creo que) entiendo que el tipo de entrada y el tipo de retorno de la función execute se obtienen al examinar la clase. Así que se me ocurrió que podría usar Exclude<> para eliminar cualquier duplicado del segundo tipo.

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

Lo cual funciona bien cuando se proporciona un tipo para el segundo parámetro de tipo:

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]

Pero Exclude<type type> devuelve never cuando sus parámetros de tipo coinciden.

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

Así que se me ocurrió un plan no tan astuto de simplemente reemplazar never con void. Lo cual falló.

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]

Pregunta

¿Hay una forma sencilla de reemplazar never?

Y si no, ¿hay otra solución que me permita eliminar los tipos duplicados del segundo espacio de la tupla y si el resultado de eso es nada, reemplazar eso con void?

Entonces :

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

sería:

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

Respuesta #1

Encontré la solución en esta respuesta de SO

Los tipos condicionales se distribuyen sobre los parámetros de tipo sin envolver. Esto significa que el tipo condicional se aplica a cada miembro de la unión. never se considera como la unión vacía. Por lo tanto, el tipo condicional nunca se aplica (ya que no hay miembros en la unión a los que aplicarlo), lo que resulta en el tipo never.

La solución simple es desactivar el comportamiento distributivo de los tipos condicionales usando una tupla:

Así que simplemente cambiando

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

a

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

resolvió mi problema.