Reputation: 2257
Let's say I have a union type that I am using with a reducer-like pattern for API calls that looks something like this:
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" },
};
When I choose a particular action, I want to be able to predict what the corresponding response will be with type safety. For example, if I were to make a function like this, I want to enforce that I am indeed returning the proper corresponding response for every request. I am not having a trouble with type safety on the input types. I am having trouble linking the output to the corresponding sub-type related to the input type.
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 👎
};
}
}
How can I indicate that I want the ["response"]
part of the object type chosen in the ["request"]
, and not just any Action["response"]
?
I would also like to be able to link these two corresponding halves of the object when making middleware functions like this, for example:
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
}
...
Upvotes: 4
Views: 316
Reputation: 33111
In order to reduce complexity and increase type safety it worth using here strategy pattern
.
Consider this example:
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 },
})
Strategy
- creates hash map data structure where key is an action name and value is a return type of your function.
Also, I have overloaded doAction
function in order to narrow return type.
Upvotes: 1