UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

1,220 lines (1,122 loc) 34.7 kB
import { IncomingHttpHeaders } from 'node:http' import { FromSchema, JSONSchema } from 'json-schema-to-ts' import { Type, TSchema, Static } from 'typebox' import { expect } from 'tstyche' import fastify, { FastifyTypeProvider, HookHandlerDoneFunction, FastifyRequest, FastifyReply, FastifyInstance, FastifyError, SafePromiseLike } from '../../fastify.js' const server = fastify() // ------------------------------------------------------------------- // Default (unknown) // ------------------------------------------------------------------- server.get('/', (req) => expect(req.body).type.toBe<unknown>()) // ------------------------------------------------------------------- // Remapping // ------------------------------------------------------------------- interface NumberProvider extends FastifyTypeProvider { validator: number serializer: number } // remap all schemas to numbers server.withTypeProvider<NumberProvider>().get( '/', { schema: { body: { type: 'string' }, querystring: { type: 'string' }, headers: { type: 'string' }, params: { type: 'string' } } }, (req) => { expect(req.headers).type.toBe<number & IncomingHttpHeaders>() expect(req.body).type.toBe<number>() expect(req.query).type.toBe<number>() expect(req.params).type.toBe<number>() } ) // ------------------------------------------------------------------- // Override // ------------------------------------------------------------------- interface OverriddenProvider extends FastifyTypeProvider { validator: 'inferenced' } server.withTypeProvider<OverriddenProvider>().get<{ Body: 'override' }>( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) } }, (req) => { expect(req.body).type.toBe<'override'>() } ) // ------------------------------------------------------------------- // TypeBox // ------------------------------------------------------------------- interface TypeBoxProvider extends FastifyTypeProvider { validator: this['schema'] extends TSchema ? Static<this['schema']> : unknown serializer: this['schema'] extends TSchema ? Static<this['schema']> : unknown } server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) }, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number>() expect(request.body.y).type.toBe<number>() expect(request.body.z).type.toBe<number>() expect(reply).type.toBeAssignableTo<FastifyReply>() } }, (req) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<number>() expect(req.body.z).type.toBe<number>() } ) expect(server.withTypeProvider<TypeBoxProvider>()).type.toBeAssignableTo<FastifyInstance>() // ------------------------------------------------------------------- // JsonSchemaToTs // ------------------------------------------------------------------- interface JsonSchemaToTsProvider extends FastifyTypeProvider { validator: this['schema'] extends JSONSchema ? FromSchema<this['schema']> : unknown serializer: this['schema'] extends JSONSchema ? FromSchema<this['schema']> : unknown } // explicitly setting schema `as const` server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } as const }, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number | undefined>() expect(request.body.y).type.toBe<string | undefined>() expect(request.body.z).type.toBe<boolean | undefined>() expect(reply).type.toBeAssignableTo<FastifyReply>() } }, (req) => { expect(req.body.x).type.toBe<number | undefined>() expect(req.body.y).type.toBe<string | undefined>() expect(req.body.z).type.toBe<boolean | undefined>() } ) server.withTypeProvider<JsonSchemaToTsProvider>().route({ url: '/', method: 'POST', schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } } as const, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number | undefined>() expect(request.body.y).type.toBe<string | undefined>() expect(request.body.z).type.toBe<boolean | undefined>() expect(reply).type.toBeAssignableTo<FastifyReply>() }, handler: (req) => { expect(req.body.x).type.toBe<number | undefined>() expect(req.body.y).type.toBe<string | undefined>() expect(req.body.z).type.toBe<boolean | undefined>() } }) // inferring schema `as const` server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } }, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number | undefined>() expect(request.body.y).type.toBe<string | undefined>() expect(request.body.z).type.toBe<boolean | undefined>() expect(reply).type.toBeAssignableTo<FastifyReply>() } }, (req) => { expect(req.body.x).type.toBe<number | undefined>() expect(req.body.y).type.toBe<string | undefined>() expect(req.body.z).type.toBe<boolean | undefined>() } ) server.withTypeProvider<JsonSchemaToTsProvider>().route({ url: '/', method: 'POST', schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } }, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number | undefined>() expect(request.body.y).type.toBe<string | undefined>() expect(request.body.z).type.toBe<boolean | undefined>() expect(reply).type.toBeAssignableTo<FastifyReply>() }, handler: (req) => { expect(req.body.x).type.toBe<number | undefined>() expect(req.body.y).type.toBe<string | undefined>() expect(req.body.z).type.toBe<boolean | undefined>() } }) expect(server.withTypeProvider<JsonSchemaToTsProvider>()).type.toBeAssignableTo<FastifyInstance>() // ------------------------------------------------------------------- // Instance Type Remappable // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { body: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'string' }, z: { type: 'boolean' } } } as const }, errorHandler: (error, request, reply) => { expect(error).type.toBe<FastifyError>() expect(request).type.toBeAssignableTo<FastifyRequest>() expect(request.body.x).type.toBe<number | undefined>() expect(request.body.y).type.toBe<string | undefined>() expect(request.body.z).type.toBe<boolean | undefined>() expect(reply).type.toBeAssignableTo<FastifyReply>() } }, (req) => { expect(req.body.x).type.toBe<number | undefined>() expect(req.body.y).type.toBe<string | undefined>() expect(req.body.z).type.toBe<boolean | undefined>() } ) // ------------------------------------------------------------------- // Request Hooks // ------------------------------------------------------------------- // Sync handlers server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.String(), z: Type.Boolean() }) }, preHandler: (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preParsing: (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preSerialization: (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preValidation: (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onError: (req, reply, error, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onRequest: (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onResponse: (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onTimeout: (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onSend: (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() } }, req => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() } ) // Async handlers server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { body: Type.Object({ x: Type.Number(), y: Type.String(), z: Type.Boolean() }) }, preHandler: async (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preParsing: async (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preSerialization: async (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, preValidation: async (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onError: async (req, reply, error, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onRequest: async (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onResponse: async (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onTimeout: async (req, reply, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() }, onSend: async (req, reply, payload, done) => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() } }, req => { expect(req.body.x).type.toBe<number>() expect(req.body.y).type.toBe<string>() expect(req.body.z).type.toBe<boolean>() } ) // ------------------------------------------------------------------- // Request headers // ------------------------------------------------------------------- // JsonSchemaToTsProvider server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { headers: { type: 'object', properties: { lowercase: { type: 'string' }, UPPERCASE: { type: 'number' }, camelCase: { type: 'boolean' }, 'KEBAB-case': { type: 'boolean' }, PRESERVE_OPTIONAL: { type: 'number' } }, required: ['lowercase', 'UPPERCASE', 'camelCase', 'KEBAB-case'] } as const } }, (req) => { expect(req.headers.lowercase).type.toBe<string>() expect(req.headers.UPPERCASE).type.toBe<string | string[] | undefined>() expect(req.headers.uppercase).type.toBe<number>() expect(req.headers.camelcase).type.toBe<boolean>() expect(req.headers['kebab-case']).type.toBe<boolean>() expect(req.headers.preserve_optional).type.toBe<number | undefined>() } ) // TypeBoxProvider server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { headers: Type.Object({ lowercase: Type.String(), UPPERCASE: Type.Number(), camelCase: Type.Boolean(), 'KEBAB-case': Type.Boolean(), PRESERVE_OPTIONAL: Type.Optional(Type.Number()) }) } }, (req) => { expect(req.headers.lowercase).type.toBe<string>() expect(req.headers.UPPERCASE).type.toBe<string | string[] | undefined>() expect(req.headers.uppercase).type.toBe<number>() expect(req.headers.camelcase).type.toBe<boolean>() expect(req.headers['kebab-case']).type.toBe<boolean>() expect(req.headers.preserve_optional).type.toBe<number | undefined>() } ) // ------------------------------------------------------------------- // TypeBox Reply Type // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send('hello') res.send(42) res.send({ error: 'error' }) expect(res.code(200).send).type.toBe<((...args: [payload: string]) => typeof res)>() expect(res.code(400).send).type.toBe<((...args: [payload: number]) => typeof res)>() expect(res.code(500).send).type.toBe<((...args: [payload: { error: string }]) => typeof res)>() } ) // ------------------------------------------------------------------- // TypeBox Reply Type (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { res.send('hello') res.send({ msg: 'hello' }) res.send({ error: 'error' }) } ) // ------------------------------------------------------------------- // TypeBox Reply Type: Non Assignable // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { expect(res.send).type.not.toBeCallableWith(false) } ) // ------------------------------------------------------------------- // TypeBox Reply Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { expect(res.send).type.not.toBeCallableWith(false) } ) // ------------------------------------------------------------------- // TypeBox Reply Return Type // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return 42 case 3: return { error: 'error' } } } ) // ------------------------------------------------------------------- // TypeBox Reply Return Type (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return { msg: 'hello' } case 3: return { error: 'error' } } } ) // ------------------------------------------------------------------- // TypeBox Reply Return Type: Non Assignable // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: Type.String(), 400: Type.Number(), 500: Type.Object({ error: Type.String() }) } } }, // @ts-expect-error Type 'boolean' is not assignable to type 'string | number | void | { error: string; }'. async (_, res) => { return false } ) // ------------------------------------------------------------------- // TypeBox Reply Return Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<TypeBoxProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: Type.String() }, 'application/json': { schema: Type.Object({ msg: Type.String() }) } } }, 500: Type.Object({ error: Type.String() }) } } }, // @ts-expect-error Type 'boolean' is not assignable to type 'string | void | { msg: string; } | { error: string; }'. async (_, res) => { return false } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, (_, res) => { res.send('hello') res.send(42) res.send({ error: 'error' }) expect(res.code(200).send).type.toBe<((...args: [payload: string]) => typeof res)>() expect(res.code(400).send).type.toBe<((...args: [payload: number]) => typeof res)>() expect(res.code(500).send).type.toBe< ((...args: [payload: { [x: string]: unknown; error?: string }]) => typeof res) >() } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, (_, res) => { res.send('hello') res.send({ msg: 'hello' }) res.send({ error: 'error' }) } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type: Non Assignable // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { expect(res.send).type.not.toBeCallableWith(false) } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type: Non Assignable (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { expect(res.send).type.not.toBeCallableWith(false) } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { 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 switch (option) { case 1: return 'hello' case 2: return 42 case 3: return { error: 'error' } } } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { const option = 1 as 1 | 2 | 3 switch (option) { case 1: return 'hello' case 2: return { msg: 'hello' } case 3: return { error: 'error' } } } ) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return: Non Assignable // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, // @ts-expect-error Type 'boolean' is not assignable to type 'string | number | void | { [x: string]: unknown; error?: string | undefined; }'. async (_, res) => { return false } ) // https://github.com/fastify/fastify/issues/4088 server.withTypeProvider<JsonSchemaToTsProvider>().get('/', { schema: { response: { 200: { type: 'string' } } } as const // @ts-expect-error Type '{ foo: number; }' is not assignable to type 'string | void | Promise<string | void>'. }, (_, res) => { return { foo: 555 } }) // ------------------------------------------------------------------- // JsonSchemaToTs Reply Type Return: Non Assignable (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, // @ts-expect-error Type 'Promise<boolean>' is not assignable to type 'string | void | { [x: string]: unknown; msg?: string | undefined; } | { [x: string]: unknown; error?: string | undefined; } | Promise<string | void | { [x: string]: unknown; msg?: string | undefined; } | { [x: string]: unknown; error?: string | undefined; }>'. async (_, res) => { return false } ) // ------------------------------------------------------------------- // Reply Type Override // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(true) } ) // ------------------------------------------------------------------- // Reply Type Override (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.send(true) } ) // ------------------------------------------------------------------- // Reply Type Return Override // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>( '/', { schema: { response: { 200: { type: 'string' }, 400: { type: 'number' }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return true } ) // ------------------------------------------------------------------- // Reply Type Return Override (Different Content-types) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { return true } ) // ------------------------------------------------------------------- // Reply Status Code (Different Status Codes) // ------------------------------------------------------------------- server.withTypeProvider<JsonSchemaToTsProvider>().get( '/', { schema: { response: { 200: { content: { 'text/string': { schema: { type: 'string' } }, 'application/json': { schema: { type: 'object', properties: { msg: { type: 'string' } } } } } }, 500: { type: 'object', properties: { error: { type: 'string' } } } } as const } }, async (_, res) => { res.code(200) res.code(500) expect(res.code).type.not.toBeCallableWith(201) expect(res.code).type.not.toBeCallableWith(400) } ) // ------------------------------------------------------------------- // RouteGeneric Reply Type Return (Different Status Codes) // ------------------------------------------------------------------- server.get<{ Reply: { 200: string | { msg: string } 400: number '5xx': { error: string } } }>( '/', async (_, res) => { const option = 1 as 1 | 2 | 3 | 4 switch (option) { case 1: return 'hello' case 2: return { msg: 'hello' } case 3: return 400 case 4: return { error: 'error' } } } ) // ------------------------------------------------------------------- // RouteGeneric Status Code (Different Status Codes) // ------------------------------------------------------------------- server.get<{ Reply: { 200: string | { msg: string } 400: number '5xx': { error: string } } }>( '/', async (_, res) => { res.code(200) res.code(400) res.code(500) res.code(502) expect(res.code).type.not.toBeCallableWith(201) expect(res.code).type.not.toBeCallableWith(300) expect(res.code).type.not.toBeCallableWith(404) return 'hello' } ) // ------------------------------------------------------------------- // RouteGeneric Reply Type Return: Non Assignable (Different Status Codes) // ------------------------------------------------------------------- server.get<{ Reply: { 200: string | { msg: string } 400: number '5xx': { error: string } } }>( '/', // @ts-expect-error Type 'boolean' is not assignable to type 'string | number | void | { msg: string; } | { error: string; } | { 200: string | { msg: string; }; 400: number; '5xx': { error: string; }; }'. async (_, res) => { return true } ) // ------------------------------------------------------------------- // FastifyPlugin: Auxiliary // ------------------------------------------------------------------- interface AuxiliaryPluginProvider extends FastifyTypeProvider { validator: 'plugin-auxiliary' } // Auxiliary plugins may have varying server types per application. Recommendation would be to explicitly remap instance provider context within plugin if required. function plugin<T extends FastifyInstance> (instance: T) { instance.withTypeProvider<AuxiliaryPluginProvider>().get( '/', { schema: { body: null } }, (req) => { expect(req.body).type.toBe<'plugin-auxiliary'>() } ) } server.withTypeProvider<AuxiliaryPluginProvider>().register(plugin).get( '/', { schema: { body: null } }, (req) => { expect(req.body).type.toBe<'plugin-auxiliary'>() } ) // ------------------------------------------------------------------- // Handlers: Inline // ------------------------------------------------------------------- interface InlineHandlerProvider extends FastifyTypeProvider { validator: 'handler-inline' } // Inline handlers should infer for the request parameters (non-shared) server.withTypeProvider<InlineHandlerProvider>().get( '/', { onRequest: (req, res, done) => { expect(req.body).type.toBe<'handler-inline'>() }, schema: { body: null } }, (req) => { expect(req.body).type.toBe<'handler-inline'>() } ) // ------------------------------------------------------------------- // Handlers: Auxiliary // ------------------------------------------------------------------- interface AuxiliaryHandlerProvider extends FastifyTypeProvider { validator: 'handler-auxiliary' } // Auxiliary handlers are likely shared for multiple routes and thus should infer as unknown due to potential varying parameters function auxiliaryHandler (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction): void { expect(request.body).type.toBe<unknown>() } server.withTypeProvider<AuxiliaryHandlerProvider>().get( '/', { onRequest: auxiliaryHandler, schema: { body: 'handler-auxiliary' } }, (req) => { expect(req.body).type.toBe<'handler-auxiliary'>() } ) // ------------------------------------------------------------------- // SafePromiseLike // ------------------------------------------------------------------- const safePromiseLike = { then: new Promise<string>(resolve => resolve('')).then, __linterBrands: 'SafePromiseLike' as const } expect(safePromiseLike).type.toBeAssignableTo<SafePromiseLike<string>>() expect(safePromiseLike).type.toBeAssignableTo<PromiseLike<string>>() expect(safePromiseLike).type.not.toBeAssignableTo<Promise<string>>() // ------------------------------------------------------------------- // Separate Providers // ------------------------------------------------------------------- interface SeparateProvider extends FastifyTypeProvider { validator: string serializer: Date } server.withTypeProvider<SeparateProvider>().get( '/', { schema: { body: null, response: { 200: { type: 'string' } } } }, (req, res) => { expect(req.body).type.toBe<string>() res.send(new Date()) } )