Search code examples
node.jstypescriptexpresszod

Provide types to const middlewares for express-zod-api


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


Solution

  • 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.