Búsqueda de sitios web

Genéricos de comando y respuesta en TypeScript


Me está costando idear una buena estructura para un genérico Comando -> Respuesta.

Mi objetivo es que una función acepte un comando de una lista de comandos basada en un Tipo o Interfaz, y use esa interfaz para inferir el tipo de respuesta.

Hasta ahora he logrado inferir correctamente los data esperados basándome en la propiedad type, pero no puedo parecer entender cómo inferir el tipo de retorno del genérico.

¡Cualquier indicación hacia ejemplos similares es muy bienvenida! No he podido encontrar mucho

Ejemplo de código: Playground de TypeScript

type MappedData<E, T extends keyof E> = E[T];
type MappedType<E> = {
    [K in keyof E]: {
        type: K;
        data: MappedData<E, K>;
    };
}[keyof E];

interface ServerCommandInterface<T> {
     _res?: T;
}

interface TestCommandA extends ServerCommandInterface<{responseA: boolean}> {
    param_a: string;
}

interface TestCommandB extends ServerCommandInterface<{responseB: boolean}> {
    param_b: string;
}

interface Commands {
    command_a: TestCommandA;
    command_b: TestCommandB;
}

function execute<C extends MappedType<Commands>>(command: C): typeof command.data._res {
    // Logic (HTTP call or similar) that would result in _res type
    return null as any;
}

const result = execute({
    type: 'command_a',
    data: {
        param_a: 'param',
    }
});

console.log(result.responseA); // I expect this to be a boolean

Respuesta #1

Así que creo que lo que estás buscando es algo como el siguiente tipo de ayuda:

interface Commands {
    command_a: {
        param: { param_a: string },
        response: { responseA: boolean }
    },
    command_b: {
        param: { param_b: string },
        response: { responseB: boolean }
    }
}

Aquí cada propiedad de Commands tiene una clave que corresponde a la propiedad type del valor pasado a execute(). Y el valor es un tipo de objeto con dos propiedades: param y response. La propiedad param corresponde a la propiedad data del valor pasado a execute(), mientras que la propiedad response corresponde al valor devuelto por execute(). Nótese que los nombres param y response son completamente arbitrarios y podríamos haberlos llamado como quisiéramos. No habrá ningún valor de tipo Commands involucrado aquí; es solo un tipo auxiliar para permitirnos expresar la firma de llamada de execute() de una manera sencilla:

function execute<K extends keyof Commands>(
  command: { type: K, data: Commands[K]['param'] }
): Commands[K]['response'] {
    return null as any;
}

Entonces la función execute() es genérica en el parámetro de tipo K, limitado a ser una de las claves de Commands. Luego, el parámetro de execute() tiene una propiedad type de tipo K, y una propiedad data de tipo Commands[K]['param']. (Estamos usando tipos de acceso indexado para obtener el tipo de la propiedad clave K de Commands y luego obtener la propiedad con clave "param" de eso). Y el tipo de retorno es Commands[K]['response'].

Veamos si funciona:

const result = execute({
    type: 'command_a',
    data: {
        param_a: 'param',
    }
});

Aquí el compilador infiere "command_a" para K, y por lo tanto la firma de la llamada se especifica como

/* function execute<"command_a">(command: {
    type: "command_a";
    data: {
        param_a: string;
    };
}): {
    responseA: boolean;
} */

Y así result es de tipo

/* const result: {
    responseA: boolean;
} */

como se esperaba:

result.responseA === true // okay

Enlace de Playground al código