Fastify decorateReply types
Extremely niche stuff here, this probably isn't relevant to 99% of people, but nevertheless, for someone this will eventually be useful.
Fastify with the TypeBox type provider is a pretty great experience. It lets you define types of route parameters, query string parameters, and responses. It gives you both runtime and static (TypeScript) type safety.
Fastify also has a system for Decorators, in which you can enhance its objects with extra methods. So, for example, you could add a new method to their reply object – the object usually called response in Express and some other systems.
We've also been using neverthrow, a TypeScript library that provides a Result type, just like in fancy languages like Rust. A Result can contain a successful value, or an error, and it preserves good types for that error, totally unlike JavaScript’s chaotic try/catch exception system. neverthrow has been mostly great for us, with some minor nits.
Neverthrow has helped with using shared application logic between Remix, tRPC, and Fastify: we can have some shared error types and 'unwrap' them in ways that each of the systems prefers. I wanted to do the same for Fastify.
So I wanted to add a method like reply.sendResult(result: Result<T, E>), and for the 'ok' value in the result to have the nice types that I would get from the reply.send(value: T) method. This is not super obvious, because all of the examples of using decorateReply don't include any of its generic parameters. Took a while but I figured it out. The incantation is:
declare module "fastify" {
interface FastifyReply<
RouteGeneric,
RawServer,
RawRequest,
RawReply,
ContextConfig,
SchemaCompiler,
TypeProvider,
ReplyType,
> {
sendResult<T extends ReplyType, E extends MappableErrors>(
result: Result<T, E>
): FastifyReply;
}
}It's verbose because we have to repeat all of the existing type parameters for the generic FastifyReply type in order to access that last ReplyType parameter, but after we've done that, it merges into existing types.
And here, MappableErrors is the base class for our known set of Error subclasses. You probably have different ones. And then the functional part of the plugin looks like this:
export const fastifySendResultPlugin = fp(async (fastify: Fastify) => {
fastify.decorateReply("sendResult", function (result) {
return result.match(
(value) => {
return this.send(value);
},
(error) => {
// Map your errors to fastify responses…
}
);
});
});Not too bad! Lets us have both good Result types and strict types inferred from the schemas in Fastify routes. Roughly the same trick would apply for any other kind of reply decorator for a wrapped type, or the same idea for another Result library, like true-myth or Effect.