DEV Community

loading...

Is Declaration Merging and Generic Inheritance at the Same Time Impossible?

Ethan Arrowood
Microsoft Software Engineer
惻3 min read

Fastify is a fast and low overhead web framework for Node.js. It supports http, https, and http2 server types. The Fastify v3 type system provides a generic property so the user can specify which type of server they want to create; this generic property is then used to determine the type of the server request and reply objects.

The following definitions are a simplified abstraction from the actual Fastify v3 type system. You can explore the complete source code here

The start of the definition defines the list of types the server generic can be, as well as the main function fastify.

import http from 'http'
import https from 'http'
import http2 from 'http2'

type ServerTypes = http.Server | https.Server | http2.Http2Server

declare function fastify<Server extends ServerTypes>(): FastifyInstance<Server>

The FastifyInstance definition depends on two generic expressions, one for determining the base request type, and the other for reply.

type RequestExpression<Server extends ServerTypes> = (
  Server extends http.Server | https.Server
  ? http.IncomingMessage
  : http2.Http2ServerRequest 
)

type ReplyExpression<Server extends ServerTypes> = (
  Server extends http.Server | https.Server
  ? http.ServerResponse
  : http2.Http2ServerResponse
)

interface FastifyInstance<
  Server extends ServerTypes,
  Request = RequestExpression<Server>,
  Reply = ReplyExpression<Server>
> {
  request: FastifyRequest<Request>,
  reply: FastifyReply<Reply>
}

These Request and Reply generics are then passed to the FastifyRequest and FastifyReply definitions. These make use of generic inheritance to add additional properties to the base request and reply types.

type FastifyRequest<Request> = Request & {
  body: unknown,
  query: unknown
}

type FastifyReply<Reply> = Reply & {
  sent: boolean,
  code(c: number): FastifyReply<Reply>
}

Fastify supports plugins for decorating the server, request, and reply instances with additional properties from the user.

function myPlugin (inst, opts, next) {
  inst.decorateRequest('myPluginProp', 'super_secret_string')
  inst.decorareReply('myPluginProp', 5000)
}

But how can we update the type system to acknowledge these new properties? Additionally, to support module based plugins (i.e. downloadable from npm), the type override should just work by importing the plugin into a project (i.e. import myPlugin from 'myPlugin'). We can try to use declaration merging on the FastifyRequest and FastifyReply types:

declare module 'fastify' {
  type FastifyRequest = {
    myPluginProp: string
  }
  type FastifyReply = {
    myPluginProp: number
  }
}

Sadly this does not work; you cannot merge type declarations. What if we try rewriting the type declarations as interfaces?

interface FastifyRequest<Request> extends Request {
  raw: Request;
  body: unknown;
  query: unknown;
}

interface FastifyReply<Reply> extends Reply {
  raw: Reply;
  sent: boolean;
  code(c: number): FastifyReply<Reply>;
}

Sadly, this also does not work; it throws an error:

An interface can only extend an object type or intersection of object types with statically known members.

If we use types, then we can't support declaration merging. And if we use interfaces, then we can't support generic inheritance. šŸ¤”

šŸ’” What if we use both types and interfaces?

  1. Encapsulate the custom properties into interfaces
interface FastifyRequestInterface<Request> {
  raw: Request;
  body: unknown;
  query: unknown;
}

interface FastifyReplyInterface<Reply> {
  raw: Reply;
  sent: boolean;
  code(c: number): FastifyReply<Reply>;
}
  1. Replace the { ... } part of the type declaration with these new interfaces
type FastifyRequest<Request> = Request & FastifyRequestInterface<Request>

type FastifyReply<Reply> = Reply & FastifyReplyInterface<Reply>

Now, if the user wants to add custom properties too this they can use declaration merging on the FastifyRequestInterface and FastifyReplyInterface interfaces, and the type declarations can still inherit from the generic parameters!

declare module 'fastify' {
  interface FastifyRequestInterface {
    myPluginProp: string
  }
  interface FastifyReplyInterface {
    myPluginProp: number
  }
}

And in some implementation file:

import fastify from 'fastify'
import myPlugin from 'myPlugin'

const server = fastify()

server.register(myPlugin)

server.get('/', (request, reply) => {
  request.myPluginProp // -> āœ… string
  reply.myPluginProp // -> āœ… number
})

šŸŽ‰ Thus, no it is not impossible to support both declaration merging and generic inheritance at the same time!

--

Thank you for reading! If you enjoyed this article consider following myself on Twitter @ArrowoodTech.

To learn more about Fastify checkout our GitHub repository or our website.

Discussion (7)

Collapse
iampeterbanjo profile image
Peter Banjo

I really would like to use this solution but I don't see enough information in the code snippets to apply to my specific problem which already includes declaration merging. Could you please publish a full example on Github?

Collapse
iampeterbanjo profile image
Peter Banjo

I found a solution here github.com/fastify/help/issues/122 any thoughts?

Collapse
ethanarrowood profile image
Ethan Arrowood Author

You can see a much more verbose example in the fastify type definitions on the master branch. github.com/fastify/fastify/blob/ma...

Collapse
mateiadrielrafael profile image
Matei Adriel

Why not just make a ServerBuilder class with a register method which returns a ServerBuilder with the plugin added to some array and some type params changed? Then a .create method would just return a server having the type parameters of the last ServerBuilder instance in the chain

Collapse
ethanarrowood profile image
Ethan Arrowood Author

Fastify is not written in TypeScript so we need to come up with alternative ways of defining types!

Collapse
michaeljota profile image
Michael De Abreu

Amazing. Sure not something you would use everyday, but an incredible clever solution for something complex as this. Thank you.

Collapse
ethanarrowood profile image
Ethan Arrowood Author

Yeah exactly! When I initially discovered this problem I found a lot of StackOverflow responses saying 'this is impossible' or 'not supported'.