Home > OS >  Type 'any[]' is not assignable to type '[id: string]'
Type 'any[]' is not assignable to type '[id: string]'

Time:03-30

I'm trying to create a generic API type, which has properties that are functions that return Promises.

export type API = {
  [key: string]: <Params extends any[], Response>(...params: Params) => Promise<Response>,
}

export interface UserResponse {
  data: User
}

export interface User {
  id: string;
  name: string;
}

export const api: API = {
  get: (id: User['id']): Promise<UserResponse> => Promise.resolve({
    data: {
      id,
      name: 'name'
    }
  })
};

This gives me the error on get:

Type '(id: User['id']) => Promise<UserResponse>' is not assignable to type '<Params extends any[], Response>(...params: Params) => Promise<Response>'.
  Types of parameters 'id' and 'params' are incompatible.
    Type 'Params' is not assignable to type '[id: string]'.
      Type 'any[]' is not assignable to type '[id: string]'.
        Target requires 1 element(s) but source may have fewer.

How can I get the generic type to accept id: string as an argument to the API method?

Playground

CodePudding user response:

This is because you are using template literals incorrectly. The simplest (and most "wrong" answer) is the following... This will break in a lot of use cases, and your generic...

export type API = {
  [key: string]: (...params: any[]) => Promise<Response>,
}

Instead lets go the route you need this generic:

Lets ask ourselves what the API type is actually asking for, it is defining all its key/value pairs as having any amount of parameters, which is technically true, but will expose the API const incorrectly, allowing you to do something like this

API.get() //<-- No error thrown here, since it uses the API typing correctly.
API.get(true, false, 'foo', 'bar') //<-- This also is allowed

Hence why we are thrown this error Target requires 1 element(s) but source may have fewer. Basically outlining that on the exported end of the API someone could use it incorrectly.

First option: Explicitly type all members of the API. This is the preferred, and safest option to ensure the shape of the API.

export type API = {
  get: (id: string) => Promise<Response>
  //set: (id: string) => Promise<Response>
  //...
}

Second Option: Let TS inference your API. For immutability assign the as const assertion.

const api = {
  get: (id: User['id']): Promise<Response> => Promise.resolve({
    data: {
      id,
      name: 'name'
    }
  }),
} //as const
// {
//   get: (id: User['id']) => Promise<Response>;
// }

Third option: Create a wrapper function which will inference a return value, this is a common trick in TS abusing function inferencing to determine a type based on an assignment. This is the best option if you want to use TS to enforce API have a certain type, esp. if multiple developers will be touching this code (but don't want to explicitly type the API). This is also the worst option in terms of ease of readability, obfuscating types to those who may not be familiar with TS, and may make it harder to trace what api is.

const returnAPI = <
  T extends Record<string, (...args: any[]) => Promise<Response>>
>(o: T): {
  [K in keyof T]: T[K]
} => o

const api = returnAPI({
  get: (id: User['id']): Promise<Response> => Promise.resolve({
    data: {
      id,
      name: 'name'
    }
  }),
})

We use the extends keyword to denote that T must be of a certain shape, in this case, consisting of keys extending string value, and their value must be a function which always returns Promise<Response>, this can be modified as needed.

Check out the Playground to find the best option for you.

  • Related