0

I have data structure like this:

const endpoints = {
  async Login: (params) => { ... }
  async Register: (params) => { ... }
}

Now I want to specify that every item in this object must accept params object and return a promise.

I can do something like this:

interface EndpointMap {
  [endpointName: string]: (
    params: Record<string, any>
  ) => Promise<any>;
}

This works well. But there's a downside to this.

If I do elsewhere that for example endpoint : keyof typeof endpoints the result would only be string. If I remove the EndpointMap interface, I'd get a String Union of all the keys on the endpoint object. Much better!

Is there a way to have the best of both worlds?

Thanks!

1
  • But there's a downside to this Yes. it will accept object value { foo: (params) =>{} } as well. If you are aware about keys, you should specify them. That would even help TS in giving you intellisense. Commented Apr 21, 2020 at 8:56

2 Answers 2

1

You can achieve this by creating endpoints using identity function:

interface EndpointMap {
  [endpointName: string]: (
    params: Record<string, any>
  ) => Promise<any>;
}

const createEndpoints = <TMap extends EndpointMap>(map: TMap) => map;

const endpoints = createEndpoints({
  login: async (params) => ({}),
  register: async (params) => ({})
});

/*
  type of 'endpoints' variable is: 
  {
      login: (params: Record<string, any>) => Promise<{}>;
      register: (params: Record<string, any>) => Promise<{}>;
  }
*/

We've declared identity function with EndpointMap constraint on generic type parameter so typescript will verify that passed parameter has appropriate structure. Additionally parameter type will be inferred and valid keys' type won't be widened to string, so:

type Keys = keyof typeof endpoints; // will be "login" | "register"

Playground

Sign up to request clarification or add additional context in comments.

1 Comment

It would be nicer if I wouldn't have to do this wrap, but it works! I'll see if it's worth it to use this trick on many places for the sake of readability, but I'll definitely do it here and there in some utility code. Thanks!
1

One way to achieve this is instead of giving keys as type string define the keys in the type EndpointMap:

type EndpointMap = {
    [key in 'Login' | 'Register']: (
        params: any
    ) => Promise<any>
}

const endpoints: EndpointMap = {
    Login: (params) => { ... },
    Register: (params) => {...  }
}

// Valid
const oneKey: keyof typeof endpoints = 'Login'

// Type '"Random"' is not assignable to type '"Login" | "Register"'
const otherKey: keyof typeof endpoints = 'Random'

1 Comment

key in is nice, but at this point I could just write the string union. reuse it here and also use it elsewhere and I wouldn't have to use keyof typeof at all. The point of keyof typeof for me is that that the object itself is the source of truth and I don't need to manage the list of endpoints on two places.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.