I'm currently trying to figure out how to create a typed function that will call the following object based on its key:
const commandOrQuery = {
CREATE_USER_WITH_PASSWORD: CreateUserCommandHandler,
GET_USERS: GetUsersQueryHandler,
};
In which commandOrQuery
is of the shape: Record<string, Function>
I'd like to create a dispatch
function that will:
export const dispatch = ({type, args}) => commandOrQuery[type](args)
Where:
type
is the key of the commandOrQuery
object.
args
are the arguments of the commandOrquery[type]
function.
And dispatch
returns the type of the object.
Meaning that if I do the following:
const result = await dispatch({
type: "GET_USERS",
args : {
// tenantId is exclusive to "GET_USERS"
tenantId: string;
}
});
// result will have the following shape {name: string, id: string}[] which
// is exclusive to "GET_USERS" and GetUsersQueryHandler
I've come very close by defining the types separately, like this:
export type Mediator =
| {
type: "CREATE_USER_WITH_PASSWORD";
arg: CreateUserCommand | typeof CreateUserCommandHandler;
}
| {
type: "GET_USERS";
arg: GetUsersQuery | typeof GetUsersQueryHandler;
}
| {
type: "GET_USER_PROFILE";
arg: GetUserProfile | typeof GetUserProfileQueryHandler;
};
And defining the dispatch like this:
export const dispatch = ({type, args}: Mediator) => commandOrQuery[type](args)
But I'm currently lacking the return type. I'd like TypeScript to infer the ReturnType automatically after I provide the type
in the argument.
Is this even possible?
I've been researching for a couple of hours now:
//file: bounded_contexts/CreateUserWithPasswordCommand.ts
export type CreateUserCommand = {
email: string;
password: string;
};
export async function CreateUserCommandHandler(cmd: CreateUserCommand) {
console.log("Creating User");
return true;
}
// file: bounded_contexts/GetUserProfileQuery.ts
export type GetUserProfileQuery = {
userId: string;
};
export async function GetUserProfileQueryHandler(query: GetUserProfileQuery) {
console.log("Searching in db the userId", query.userId);
return {
firstName: "Carlos",
lastName: "Johnson",
dateOfBirth: new Date()
};
}
// file: bounded_contexts/GetUsersQuery.ts
export type GetUsersQuery = {};
export async function GetUsersQueryHandler(q: GetUsersQuery) {
return {
users: [
{
id: "id-1",
name: "Julian Perez"
},
{
id: "id-2",
name: "Adam Smith"
}
]
};
}
// file: index.ts
import { dispatch } from "./mediator";
(async () => {
// Result should be boolean
const result = await dispatch({
type: "CREATE_USER_WITH_PASSWORD",
arg: {
email: "[email protected]",
password: "the password"
}
});
// result2 should be { users: {name: string; id: string }[]
const result2 = await dispatch({
type: "GET_USERS",
arg: {}
});
// resul3 should be { firstName: string; lastName: string; dateOfBirth: Date}
const result3 = await dispatch({
type: "GET_USER_PROFILE",
arg: {
userId: "the user Id"
}
});
})();
// file: mediator.ts
import {
CreateUserCommandHandler,
CreateUserCommand
} from "./bounded_contexts/CreateUserWithPasswordCommand";
import {
GetUsersQueryHandler,
GetUsersQuery
} from "./bounded_contexts/GetUsersQuery";
import {
GetUserProfileQueryHandler,
GetUserProfileQuery
} from "./bounded_contexts/GetUserProfileQuery";
const commandsOrQueries = {
CREATE_USER_WITH_PASSWORD: CreateUserCommandHandler,
GET_USERS: GetUsersQueryHandler,
GET_USER_PROFILE: GetUserProfileQueryHandler
};
type Mediator =
| {
type: "CREATE_USER_WITH_PASSWORD";
arg: CreateUserCommand | typeof CreateUserCommandHandler;
}
| {
type: "GET_USERS";
arg: GetUsersQuery | typeof GetUsersQueryHandler;
}
| {
type: "GET_USER_PROFILE";
arg: GetUserProfileQuery | typeof GetUserProfileQueryHandler;
};
export function dispatch({ arg, type }: Mediator) {
return commandsOrQueries[type](arg as any);
}
CodePudding user response:
The problem with
const dispatch = ({ type, arg }: Mediator) => commandsOrQueries[type](arg);
is that the compiler cannot see the correlation between commandsOrQueries[type]
and arg
, even though Mediator
is a discriminated union type. This is the subject of microsoft/TypeScript#30581.
The approach recommended at microsoft/TypeScript#47109 is to make the function generic in a keylike type parameter, and represent the correlation between input and output in terms of mapped types on this type parameter. It's a bit tricky.
It could look like this. First we should use a dummy variable name to initially assign your commandsOrQueries
:
const _commandsOrQueries = {
CREATE_USER: CreateUserCommandHandler,
GET_USERS_QUERY: GetUsersQueryHandler,
GET_USER_PROFILE: GetUserProfileQueryHandler
};
Then we do some type manipulation to represent the operation of this in terms of some mapped types:
type CommandsOrQueries = typeof _commandsOrQueries;
type Commands = keyof CommandsOrQueries; // "CREATE_USER" | "GET_...
type InputMap = { [K in Commands]: Parameters<CommandsOrQueries[K]>[0] };
type OutputMap = { [K in Commands]: ReturnType<CommandsOrQueries[K]> };
const commandsOrQueries: { [K in Commands]:
(arg: InputMap[K]) => OutputMap[K] } = _commandsOrQueries;
So, commandsOrQueries
has the same value as _commandsOrQueries
, and it's also of the same type, but now the compiler sees that type as a generic operation on keys K
in Commands
.
Now we can define the Mediator
type as a distributive object type as coined in ms/TS#47109:
type Mediator<K extends Commands = Commands> =
{ [P in K]: { type: P, arg: InputMap[P] } }[K]
So Mediator<K>
is generic, and if you specify the type argument you get just the union member you care about. Like Mediator<"CREATE_USER">
is {type: "CREATE_USER", arg: CreateUserCommand}
. But if you just write Mediator
, then K
defaults to Commands
and you get the original full union:
type M = Mediator
/* type M = {
type: "CREATE_USER";
arg: CreateUserCommand;
} | {
type: "GET_USERS_QUERY";
arg: GetUsersQuery;
} | {
type: "GET_USER_PROFILE";
arg: GetUserProfileQuery;
} */
And now we can define and implement dispatch
:
const dispatch = <K extends Commands>({ type, arg }: Mediator<K>) =>
commandsOrQueries[type](arg) // okay
// const dispatch: <K extends Commands>({ type, arg }: Mediator<K>) => OutputMap[K]
This compiles without error; the compiler can see that commandsOrQueries[type]
is a function which accepts an InputMap[K]
and productes an OutputMap[K]
, and since arg
is an InputMap[K]
, the compiler is happy.
Let's test using it:
async function test() {
const result = await dispatch({
type: "CREATE_USER",
arg: {
email: "[email protected]",
password: "the password"
}
});
// Creating User
result // boolean
// result2 should be { users: {name: string; id: string }[]
const result2 = await dispatch({
type: "GET_USERS_QUERY",
arg: {}
});
console.log(result2.users.map(x => x.name.toUpperCase()).join("; "));
// "JULIAN PEREZ; ADAM SMITH"
}
test();
Looks good. The compiler has the correct types for the results as well.