I have the following scenario:
public async handle(
handler: WorkflowHandlerOption,
payload: <how_to_type_it?>,
): Promise<StepResponseInterface> {
switch (handler) {
case WorkflowHandlerOption.JOB_APPLICATION_ACTIVITY: {
const {
entity_id: jobApplicationEntityId,
status: jobApplicationStatus,
} = await this.jobApplicationActivityHandler.execute(payload);
// ...other staff
break;
}
case WorkflowHandlerOption.BILLING_FEE: {
const { entity_id: billingFeeId, status: billingFeeStatus } =
await this.billingFeeHandler.execute(payload);
// ...other staff
break;
}
case WorkflowHandlerOption.DOCUMENT_GENERATION: {
const { entity_id: documentId, status: documentStatus } =
await this.documentGenerationHandler.execute(payload);
// ...other staff
break;
}
case WorkflowHandlerOption.UPDATE_JOB: {
const { entity_id: jobId, status: jobStatus } =
await this.jobHandler.execute(payload);
// ...other staff
break;
}
case WorkflowHandlerOption.DOCUMENT_GENERATION_ACTIVITY: {
const {
entity_id: documentActivityId,
status: documentActivityStatus,
} = await this.documentGenerationActivityHandler.execute(payload);
// ...other staff
break;
}
default:
throw new BadRequestException('Handler not found');
}
// ...other staff
}
I have many handlers which are getting completely different payloads, how should I type the payload param?
Imagine there will be like 50 cases..
The single option I have found it is to use Class Validator to validate the payload at every case.
I would want to use only typescript types and not third party libraries
>Solution :
Create a type that is a union of tuple types that represents the mapping of all the combinations of arguments your function accepts.
For example:
enum MyEnum { A, B, C }
type HandlerArgs =
| [handler: MyEnum.A, payload: { a: string }]
| [handler: MyEnum.B, payload: { b: number }]
| [handler: MyEnum.C, payload: { c: boolean }]
The HandlerArgs type binds specific payloads with specific enum values.
Now you can write an implementation like this:
class Foo {
async handle([handler, payload]: HandlerArgs) {
switch(handler) {
case MyEnum.A: {
console.log(payload.a) // fine
break
}
case MyEnum.B: {
console.log(payload.b) // fine
break
}
case MyEnum.C: {
console.log(payload.c) // fine
break
}
}
}
}
Here you can see that the type of payload is getting correctly narrowed to the payload type in the arguments tuple that was matched.
You could even pull the payload types from the arguments of the functions you plan to pass those payloads to. You would do this by using the Parameters type to find the arguments of the functions and and derive that payload types from that.
For example:
enum MyEnum { A, B, C }
type HandlerArgs =
| [handler: MyEnum.A, payload: Parameters<Foo['aHandler']['execute']>[0]]
| [handler: MyEnum.B, payload: Parameters<Foo['bHandler']['execute']>[0]]
| [handler: MyEnum.C, payload: Parameters<Foo['cHandler']['execute']>[0]]
class Foo {
// mock some services this class uses
declare aHandler: { execute: (payload: { a: string }) => void }
declare bHandler: { execute: (payload: { b: number }) => void }
declare cHandler: { execute: (payload: { c: boolean }) => void }
async handle([handler, payload]: HandlerArgs) {
switch(handler) {
case MyEnum.A: {
this.aHandler.execute(payload)
break
}
case MyEnum.B: {
this.bHandler.execute(payload)
break
}
case MyEnum.C: {
this.cHandler.execute(payload)
break
}
}
}
}
Here Parameters<Foo['aHandler']['execute']>[0] drills into the type of Foo.aHandler.execute and finds the parameters of that function, and then uses the first parameter as the payload type.