Using enum in generic for router

Viewed 121

I have an enum with a list of routes.

enum ServerRoutes {
  WHISPER_SECRET = 0,
  SHOUT_SECRET = 1,
  SHOUT_SECRET_MULTIPLE_TIMES = 2
}

I'd like to create a ServerRouter that has compile-time guarantees that every route is handled. I'd like to do this using a generic so that I can later on create a ClientRouter

type Router<T> = { [route in keyof T]: () => void }
type ServerRouter = Router<ServerRoutes>;

So far so good. Everything above compiles fine. However, when I try to actually implement a ServerRouter, I get an error. Here is the code that generates the error:

const serverRouter: ServerRouter = {
  WHISPER_SECRET: () => {},
  SHOUT_SECRET: () => {},
  SHOUT_SECRET_MULTIPLE_TIMES: () => {}
}

And here is the error:

Type '{ WHISPER_SECRET: () => void; SHOUT_SECRET: () => void; SHOUT_SECRET_MULTIPLE_TIMES: () => void; }' is not assignable to type 'ServerRoutes'.

Unfortunately, the error doesn't really explain what the problem is other than "not assignable". How can I either fix my code or use another strategy to acheive the same goal?

2 Answers

Enum under the hood creates sophisticated structure which, as mentioned here, not so easy to iterate. This structure looks like this:

(function (ServerRoutes) {
    ServerRoutes[ServerRoutes["WHISPER_SECRET"] = 0] = "WHISPER_SECRET";
    ServerRoutes[ServerRoutes["SHOUT_SECRET"] = 1] = "SHOUT_SECRET";
    ServerRoutes[ServerRoutes["SHOUT_SECRET_MULTIPLE_TIMES"] = 2] = "SHOUT_SECRET_MULTIPLE_TIMES";
})(ServerRoutes || (ServerRoutes = {}));

In your case try use class instead of enum and it will work fine. I've created example here

class ServerRoutes {
  WHISPER_SECRET = 0
  SHOUT_SECRET = 1
  SHOUT_SECRET_MULTIPLE_TIMES = 2
}

As you can see here you are not the first to want to use keyof with an array, and it looks as though there is still not a clean solution in TypeScript to get what you are looking for.

However, alternatively you could create an interface to specify the contract for your serverRouter e.g.

type RouterFunction = () => void

interface IServerRouter {
  WHISPER_SECRET: RouterFunction
  SHOUT_SECRET: RouterFunction
  SHOUT_SECRET_MULTIPLE_TIMES: RouterFunction
}

const serverRouter: IServerRouter = {
  WHISPER_SECRET: () => {},
  SHOUT_SECRET: () => {},
  SHOUT_SECRET_MULTIPLE_TIMES: () => {}
}

You can see this code in this TypeScript Playground Link.

This approach is explicit and does not require the specification of redundant numeric values like you have in an enum(!).

Related