Search code examples
node.jstypescripttypescript-types

TypeScript shared configuration object with typehinting


I am developing an API library and I am curious about how should endpoint configuration problem should be approached in Node.js with TypeScript. I want all endpoint configuration to be contained within one entity.

I currently have this approach in place:

ApiConstants.ts
------------------------
const BASE_DOMAIN = 'https://api.example.com';

export default Object.freeze({
  BASE_DOMAIN: {
    V1: `${BASE_DOMAIN}/v0.1`,
    V2: `${BASE_DOMAIN}/v0.2`,
    V3: `${BASE_DOMAIN}/v0.3`,
  },

  PATH: {
    CATS: '/animals/cats',
  },
});

It does the job, I can use it in any class by importing it and accessing the values. The problem is that I want to restrict functions to only accept values declared within this object. When request constructing function should display invalid type intellisense when value is passed which is not a part of this object.

Desired type would look something like this. Path must be declared within ApiConstants.PATH object.

function makeRequest(path: ApiConstants.PATH) {
  ...
}

How can such behavior be achieved?


Solution

  • function makeRequest(path: keyof typeof ApiConstants.PATH) {
      // ...
    }
    

    See in Playground

    In general, you probably want to do something like

    const ApiConstants = Object.freeze({
      BASE_DOMAIN: {
        V1: `${BASE_DOMAIN}/v0.1`,
        V2: `${BASE_DOMAIN}/v0.2`,
        V3: `${BASE_DOMAIN}/v0.3`,
      },
    
      PATH: {
        CATS: '/animals/cats',
      },
    });
    type ApiConstants = typeof ApiConstants;
    
    export default ApiConstants;
    

    to avoid having to use typeof ApiConstants all the time. Note that this means ApiConstants is both a value and a type—Typescript is OK with this, and will know from context what you’re doing, but some programmers find it confusing. A common naming convention is to use an initial lowercase letter for the value, and an initial uppercase for the type, as in const apiConstants = /*…*/; and type ApiConstants = typeof apiConstants;.

    On the other hand, it’s kind of convenient that your default export is both the value and the type.

    You might also want to add

    export type ApiPaths = keyof ApiConstants['PATH'];
    

    We use ['PATH'] because we’re using the type ApiConstants here, not the value. We could still use the value but we’d have to add typeof to it again, as in keyof typeof ApiConstants.PATH.