I have a NodeJS server, where I use express-zod-api
to validate inputs and responses. We chain middlewares, using something like this:
const workingExample = defaultEndpointsFactory.addMiddleware(createMiddleware({
input: z.object({}),
middleware: async () => {
return {
id: "an id",
}
}
})).addMiddleware(createMiddleware({
input: z.object({}),
middleware: async ({options}) => {
console.log(`this is typed as a string: ${options.id}`)
return {
...options,
something: "else",
}
},
}))
However, I want to use the middlewares like lego - I don't always want the same middleware in the same order, so I have extracted them separate constants:
const middlewareA = createMiddleware({
input: z.object({}),
middleware: async () => {
return {
id: "an id",
}
},
})
const middlewareB = createMiddleware({
input: z.object({}),
middleware: async ({ options }) => {
console.log(`this fails because option is "unknown": ${options.id}`)
return {
...options, // TS2698: Spread types may only be created from object types.
something: "else",
}
},
})
const brokenExample = defaultEndpointsFactory
.addMiddleware(middlewareA)
.addMiddleware(middlewareB)
This gives me TS errors because the options
object is unknown
. In the working example, where middlewares are directly chained, I suppose TypeScript can figure out what options
is supposed to be.
I think I need to provide additional information to TypeScript, e.g.:
const middlewareB: MiddlewareDefinition<any, any, any, any> = createMiddleware({
I could painstakingly figure out what the any
's should be, but it will make everything pretty inflexible, especially if I am mixing and matching middlewares. Is it possible to genericise middlewareA
and middlewareB
to just... do the right thing and let TS figure it out?
Sample repro: https://github.com/crummy/express-zod-middleware
I have managed to finagle a nice-ish resolution to the problem.
const middlewareA = async () => {
return {
id: "an id",
}
}
const middlewareB = async ({ options }: { options: { id: string } }) => {
return {
...options,
something: "else",
}
}
const brokenExampleFixed = defaultEndpointsFactory
.addMiddleware(createMiddleware({
input: z.object({}),
middleware: middlewareA
}))
.addMiddleware(createMiddleware({
input: z.object({}),
middleware: middlewareB,
}))
Using this method, I can share the middlewares, with only the mild additional effort of explicitly typing the parts of the input they need, and Zod + TypeScript handles all the rest.