xoux
xoux

Reputation: 3494

How can I make a check on a string to narrow it down to the strings in an enum, and have TypeScript recognize it (node example)?

Using basic node.js, in http.createServer I am taking the request, and taking the path from the request. I have a router object, keyed by paths, with values being handler functions.

// index.ts

export interface DataObject {
  queryStringObject: ParsedUrlQuery;
  method: string;
  headers: IncomingHttpHeaders;
  payload: any;
}

export type HandlerCallback = (statusCode: number, payload?: Object) => any;

export type Handler = (data: DataObject, callback: HandlerCallback) => any;

interface Router {
  ping: Handler;
  users: Handler;
  adminUsers: Handler;
  tokens: Handler;
}

// Define request router
const router: Router = {
  ping: handlers.ping,
  users: handlers.users,
  adminUsers: handlers.adminUsers,
  tokens: handlers.tokens,
};

export enum RestMethods {
  get = 'get',
  post = 'post',
  put = 'put',
  delete = 'delete',
}

export type RestHandler = {
  [method in RestMethods]: Handler;
};

Then, I have the file where handlers is defined:

interface Handlers {
  users: Handler;
  adminUsers: Handler;
  tokens: Handler;
  notFound: Handler;
  ping: Handler;
}

interface SubHandlers {
  users: RestHandler;
  adminUsers: RestHandler;
  tokens: RestHandler;
}

export const handlers: Handlers = {
  users: (data, callback) => {
    const acceptableMethods = ['get', 'post', 'put', 'delete'];
    if (acceptableMethods.indexOf(data.method) !== -1) {
      subHandlers.users[data.method](data, callback);
    }
  },
(...)

I get a typescript error on the line subhandlers.users[data.method]...

Error:(62, 14) TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Router'. No index signature with a parameter of type 'string' was found on type 'Router'.

data.method is a string because DataObject defines it that way (an incoming request could have anything in method), and subhandlers.users is of type RestHandler, which is an object with keys in the enum RestMethods. But I am doing the check acceptableMethods.indexOf(data.method) !== -1... How can I make this check so that Typescript can realize that data.method has been restricted to only the strings that are in the enum?

Upvotes: 1

Views: 227

Answers (1)

jcalz
jcalz

Reputation: 327624

There are two issues here; one is that acceptableMethods is inferred to be of type string[], whereas you want the compiler to keep track of the string literal values it contains; you can fix that easily enough with a const assertion as in:

const acceptableMethods = ["get", "post", "put", "delete"] as const;

The second issue is that TypeScript does not understand or expect that, given an array array of type T[] and a value val, checking array.indexOf(val) !== -1 should narrow val to type T. Luckily you can get this behavior by making a user-defined type guard function:

function arrayIncludes<T>(
  arr: ReadonlyArray<T>,
  val: any
): val is T {
  return arr.indexOf(val) !== -1;
}

Now you can use arrayIncludes() in place of the indexOf check, and it should work for you:

export const handlers: Handlers = {
  users: (data, callback) => {
    const acceptableMethods = ["get", "post", "put", "delete"] as const;
    if (arrayIncludes(acceptableMethods, data.method)) {
      subHandlers.users[data.method](data, callback); // okay
    }
  }
};

Does that help? Good luck!

Link to code

Upvotes: 4

Related Questions