node.jstypescriptexpress

Defining the type of an array that includes an Express controller with a validator or a spread array of validators


I'm rewriting the Local Library project from MDN in TypeScript and struggling to define the type of a controller that has validation checks.

const createAuthor: ValidatedHandler = [
  ...validate.authorData,
  async (req, res) => { /* controller */ },
];

const createGenre: ValidatedHandler = [
  validate.genre,
  async (req, res) => { /* another controller */ }
];

ValidatedHandler is a type I created and defined as follows:

import type { RequestHandler } from "express";
import type { ValidationChain } from "express-validator";

type ValidatedHandler = [...ValidationChain[], RequestHandler];

validate.authorData is an array of validators, and validate.genre is a single validator:

import { body } from "express-validator";

const authorData = [
  body("first-name")
    .trim()
    .notEmpty()
    .withMessage("First name must be specified."),
  body("family-name")
    .trim()
    .notEmpty()
    .withMessage("Family name must be specified."),
  // etc.
];

const genre = body("name")
  .trim()
  .notEmpty()
  .withMessage("Genre name must be specified.");

The spread operator in the type definition makes ValidationChain optional. If I remove ...validate.authorData or validate.genre, TypeScript has no problem with that. How can I fix that and make it so that at least one validator is required?

I've seen posts that suggest putting one type without the spread operator (i.e., [ValidationChain, ...ValidationChain[], RequestHandler]), but when I do that, VS Code has no problem with validate.genre but shows this error for ...validate.authorData:

Type 'ValidationChain | ((req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<...>, number>) => Promise<...>)' is not assignable to type 'ValidationChain'.
  Type '(req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>, number>) => Promise<...>' is missing the following properties from type 'ValidationChain': builder, not, withMessage, custom, and 117 more.

Solution

  • I've seen posts that suggest putting one type without the spread operator (i.e., [ValidationChain, ...ValidationChain[], RequestHandler])

    This is a good suggestion! It says the array must have an element at index 0 which is a ValidationChain. That's what you want.

    but when I do that, VS Code [shows an error] for ...validate.authorData

    ...validate.authorData must be known to have at least one element to satisfy the new ValidatedHandler type, but authorData's type is inferred as ValidationChain[], which is an array type with an unknown number of elements (as far as the type is concerned, its value could be []).

    You could add a const assertion to the initial value of authorData to get its type to be inferred as a tuple known to have two elements, which allows validate.authorData to be spread into ValidatedHandler:

    import type { RequestHandler } from "express";
    import type { ValidationChain } from "express-validator";
    import { body } from "express-validator";
    
    type ValidatedHandler = [
      ValidationChain, // <-- this was added
      ...ValidationChain[],
      RequestHandler,
    ];
    
    const authorData = [
      //  ^? - const authorData: readonly [ValidationChain, ValidationChain]
      body("first-name")
        .trim()
        .notEmpty()
        .withMessage("First name must be specified."),
      body("family-name")
        .trim()
        .notEmpty()
        .withMessage("Family name must be specified."),
      // etc.
    ] as const; // <-- this was added
    
    const genre = body("name")
      .trim()
      .notEmpty()
      .withMessage("Genre name must be specified.");
    
    const validate = { authorData, genre };
    
    const createAuthor: ValidatedHandler = [
      ...validate.authorData,
      async (req: unknown, res: unknown) => { /* controller */ },
    ];
    
    const createGenre: ValidatedHandler = [
      validate.genre,
      async (req: unknown, res: unknown) => { /* another controller */ }
    ];
    

    (Playground)

    This makes it so at least one validator is required. Code like the following now results in a type error:

    const oops: ValidatedHandler = [
      async (req: unknown, res: unknown) => {} // error
    ];