Búsqueda de sitios web

Genéricos para subtipos correspondientes de tipos unión en TypeScript


Supongamos que tengo un tipo unión que estoy usando con un patrón similar a un reductor para llamadas a la API que se ve algo así:

type Action = {
    request: {
        action: "create user",
        payload: { name: string, age: number },
    },
    response: { ok: true, message: "user created" },
} | {
    request: {
        action: "delete user",
        payload: { id: number },
    },
    response: { ok: true, message: "user deleted" },
};

Cuando elijo una acción en particular, quiero poder predecir cuál será la respuesta correspondiente con seguridad de tipos. Por ejemplo, si hiciera una función como esta, quiero asegurarme de que estoy devolviendo realmente la respuesta correspondiente adecuada para cada solicitud. No tengo problemas con la seguridad de tipos en los tipos de entrada. Tengo problemas para vincular la salida con el sub-tipo correspondiente relacionado con el tipo de entrada.

function doAction<T extends Action>(request: T["request"]): T["response"] {
    if (request.action === "create user") {
        const userName = payload.user.name; // Here we're good for type safety 👍
        return {
            ok: true,
            message: "user deleted", // This should error, but it doesn't 👎
        };
    } else {
        const { id } = request.payload; // Again, we're good here for type safety 👍
        return {
            ok: true,
            message: "user deleted", // Ok, but missing strong type safety here 👎 
        }; 
    }
}

¿Cómo puedo indicar que quiero la parte ["response"] del tipo de objeto elegido en el ["request"], y no solo cualquier Action["response"]?

Enlace al Playground de TypeScript

También me gustaría poder enlazar estas dos mitades correspondientes del objeto al crear funciones middleware como esta, por ejemplo:

function<T extends Action>(req: Request<T["request"]>, res: Response<T["response"]>) {
    if (req.body.action === "create user") {
        res.send({ ok: true, message: "user deleted" }); // this should not be allowed
    }
    ...

Respuesta #1

Para reducir la complejidad y aumentar la seguridad de tipos, vale la pena usar aquí patrón de estrategia.

Considera este ejemplo:

type Action = {
    request: {
        action: "create user",
        payload: { name: string, age: number },
    },
    response: { ok: true, message: "user created" },
} | {
    request: {
        action: "delete user",
        payload: { id: number },
    },
    response: { ok: true, message: "user deleted" },
};

type Strategy = {
    [Prop in Action['request']['action']]: Extract<Action, { request: { action: Prop } }>['response']
}

const strategy: Strategy = {
    'create user': { ok: true, message: "user created" },
    'delete user': { ok: true, message: "user deleted" }
};

function doAction<T extends Action['request']>(request: T): Extract<Action, { request: T }>['response']
function doAction(request: Action['request']) {
    return strategy[request.action]

}

// {
//     ok: true;
//     message: "user created";
// }
const createUser = doAction({
    action: "create user",
    payload: { name: 'string', age: 42 },
})


// {
//     ok: true;
//     message: "user deleted";
// }
const deleteUser = doAction({
    action: "delete user",
    payload: { id: 1 },
})

Parque infantil

Strategy - crea una estructura de datos de mapa hash donde la clave es el nombre de una acción y el valor es el tipo de retorno de tu función.

Además, he sobrecargado la función doAction para estrechar el tipo de retorno.

Artículos relacionados