问题
Is there a way to set up a discriminated union that lets you capture a specific type with each union member? I'm trying to write a type safe command handler along the lines of:
interface GetUsersCommand { // returns User[]
type: 'GET_USERS'
}
interface UpdateUserNameCommand { // returns void
type: 'UPDATE_USER_NAME'
userId: string
name: string
}
type Command<Result> = GetUsersCommand | UpdateUserNameCommand
class CommandExecutor {
execute<Result>(command: Command<Result>): Result {
switch (command.type) {
case 'GET_USERS': return [ user1, user2, user3 ]
case 'UPDATE_USER_NAME': updateUserName(command.userId, command.name)
}
}
}
The idea is that the CommandExecutor
would not only knows the fields in each command after narrowing, but could also verify that the return type is as required for each command. Is there a good pattern to achieve this in TypeScript?
回答1:
You can create a relation between the command type and the result type by using an overload that captures the passed in command type and a mapping interface (you could also use a result union, but extracting the result type would not cause errors for missing command types, so I favor the mapping interface in this case).
We can't ensure the return type in the implementation corresponds to the expected return type directly. The best we can do is use an extra function to validate this in the switch :
interface GetUsersCommand { // returns User[]
type: 'GET_USERS'
}
interface CommandResults {
'GET_USERS': Users[]
}
interface UpdateUserNameCommand { // returns void
type: 'UPDATE_USER_NAME'
userId: string
name: string
}
interface CommandResults {
'UPDATE_USER_NAME': void
}
type Command = GetUsersCommand | UpdateUserNameCommand
function checkResult<T extends keyof CommandResults>(type: T, result: CommandResults[T]) {
return result;
}
class CommandExecutor {
execute<T extends Command>(command: T): CommandResults[T['type']]
execute(command: Command): CommandResults[keyof CommandResults] {
switch (command.type) {
case 'GET_USERS': return checkResult(command.type, [ user1, user2, user3 ])
case 'UPDATE_USER_NAME': return checkResult(command.type, updateUserName(command.userId, command.name))
}
}
}
Playground
来源:https://stackoverflow.com/questions/57266294/typescript-discriminated-union-with-generics