Skip to content

Invalid return type in route handler with generic reply type with status codes #6020

@dangkyokhoang

Description

@dangkyokhoang

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

5.x.x

Plugin version

No response

Node.js version

Any

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

Any

Description

import Fastify from 'fastify'

const fastify = Fastify()

fastify.get<{
  Reply: {
    200: { msg: string }
    400: { error: string }
  }
}>('/', async (request, reply) => {
  reply.code(200).send({ msg: 'works' })
  reply.code(400).send({ error: 'works' })
  return { msg: `valid but typescript would complain` }
  // return {
  //   200: { msg: `invalid but typescript wouldn't complain` },
  //   400: { error: `invalid` }
  // }
})

Got TypeScript error:

No overload matches this call.
  Overload 1 of 3, '(path: string, handler: RouteHandlerMethod<Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ServerResponse<IncomingMessage>, ... 4 more ..., FastifyBaseLogger>): FastifyInstance<...>', gave the following error.
    Argument of type '(this: FastifyInstance<...>, request: FastifyRequest<{ Reply: { 200: { msg: string; }; 400: { error: string; }; }; }, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>, reply: FastifyReply<...>) => Promise<...>' is not assignable to parameter of type 'RouteHandlerMethod<Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ServerResponse<IncomingMessage>, ... 4 more ..., FastifyBaseLogger>'.
      Type 'Promise<{ msg: string; }>' is not assignable to type 'void | { 200: { msg: string; }; 400: { error: string; }; } | Promise<void | { 200: { msg: string; }; 400: { error: string; }; }>'.
        Type 'Promise<{ msg: string; }>' is not assignable to type 'Promise<void | { 200: { msg: string; }; 400: { error: string; }; }>'.
          Type '{ msg: string; }' is not assignable to type 'void | { 200: { msg: string; }; 400: { error: string; }; }'.
  Overload 2 of 3, '(path: string, opts: RouteShorthandOptionsWithHandler<Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ServerResponse<IncomingMessage>, ... 4 more ..., FastifyBaseLogger>): FastifyInstance<...>', gave the following error.
    Argument of type '(this: FastifyInstance<...>, request: FastifyRequest<{ Reply: { 200: { msg: string; }; 400: { error: string; }; }; }, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>, reply: FastifyReply<...>) => Promise<...>' is not assignable to parameter of type 'RouteShorthandOptionsWithHandler<Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ServerResponse<IncomingMessage>, ... 4 more ..., FastifyBaseLogger>'.ts(2769)

#5704 mentioned this issue but the description wasn't clear enough so that's might be why it's closed and the issue remains existing.

Link to code that reproduces the bug

Code above

Expected Behavior

return { msg: 'valid' } should be valid.

Should I send a PR to fix this? The following tests (to be added) should pass:

// -------------------------------------------------------------------
// Reply Type Return Override (Different Status Codes)
// -------------------------------------------------------------------

expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{
  Reply: {
    200: string | { msg: string }
    400: { error: string }
    '5xx': { serverError: string }
  }
}>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2 | 3 | 4
    switch (option) {
      case 1: return 'hello'
      case 2: return { msg: 'hello' }
      case 3: return { error: 'error' }
      case 4: return { serverError: 'error' }
    }
  }
))

// -------------------------------------------------------------------
// Reply Type Return Override: Non Assignable (Different Status Codes)
// -------------------------------------------------------------------

expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get<{
  Reply: {
    200: string | { msg: string }
    400: { error: string }
    '5xx': { serverError: string }
  }
}>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    return true
  }
))

Or if only Reply[200] should be returned:

// -------------------------------------------------------------------
// Reply Type Return Override (Different Status Codes)
// -------------------------------------------------------------------

expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get<{
  Reply: {
    200: string | { msg: string }
    400: { error: string }
    '5xx': { serverError: string }
  }
}>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2
    switch (option) {
      case 1: return 'hello'
      case 2: return { msg: 'hello' }
    }
  }
))

// -------------------------------------------------------------------
// Reply Type Return Override: Non Assignable (Different Status Codes)
// -------------------------------------------------------------------

expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get<{
  Reply: {
    200: string | { msg: string }
    400: { error: string }
    '5xx': { serverError: string }
  }
}>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    return { error: 'error' }
  }
))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions