Home > OS >  TypeScript error when using a template literal for bracket notation
TypeScript error when using a template literal for bracket notation

Time:02-25

I have an object named endpoints that has various methods:

const endpoints = {
  async getProfilePhoto(photoFile: File) {
    return await updateProfilePhotoTask.perform(photoFile);
  },
};

And I'm using this function to access the methods. It takes a string argument, which is used to build a template literal, and then accesses the method using bracket notation:

export const useApiTask = (
  endpointName: string,
) => {
  //const apiActionName = 'getProfilePhoto'; // When defined as a string it works
  const apiActionName = `get${endpointName}`; // But when using template literals I get a TypeScript error
  const endpointHandler = endpoints[apiActionName]; // The TypeScript error shows here
}

But using a template literal creates this TypeScript error on endpointHandler = endpoints[apiActionName]:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ getProfilePhoto(photoFile: File): Promise<string | undefined>; }'.
  No index signature with a parameter of type 'string' was found on type '{ getProfilePhoto(photoFile: File): Promise<string | undefined>; }'. ts(7053)

Any idea what's going on here?

CodePudding user response:

There are two issues with this code. First endpointName can't be any string, it must be a valid key of endpoints (minus the get prefix). So we need to have a narrower type for that. The second issue is that even if endpointName is a union of valid keys, the literal operation will still produce string unless we tell TypeScript we want a narrower type using an as const:

const endpoints = {
  async getProfilePhoto(photoFile: File) {
    return await Promise.resolve();
  },
  async getProfile(photoFile: File) {
    return await Promise.resolve();
  },
};

export const useApiTask = (
  endpointName: 'ProfilePhoto' | 'Profile', // Union of posibilities
) => {
  const apiActionName = `get${endpointName}` as const;  // Make sure the type info is preserverd
  const endpointHandler = endpoints[apiActionName]; 
}

Playground Link

You could also use a conditional type to extract the union of names to avoid repeating the names:

type EndpointNames = keyof typeof endpoints extends `get${infer Name}` ? Name : never

Playground Link

CodePudding user response:

This error is occurring because the keys of typeof endpoints are not string, but rather the literal string "getProfilePhoto" (in your example).

To address this, you can use a clever trick, template literal inference, to derive the type of the names of the keys in your endpoints which match the pattern of "strings beginning with get", and then use the type on your endpointName parameter, like this:

TS Playground

declare const updateProfilePhotoTask: { perform (file: File): Promise<unknown>; };

const endpoints = {
  async getProfilePhoto(photoFile: File) {
    return await updateProfilePhotoTask.perform(photoFile);
  },
  getANumber() {
    return Math.random();
  },
  getThisProp: 'hello',
};

// This becomes "ProfilePhoto" | "ANumber"
type EndpointFunctionNamesPrefixedByGet = typeof endpoints extends infer T ? keyof { [
  K in keyof T as K extends `get${infer Name}` ?
    T[K] extends (...params: any) => any ? Name : never
    : never
]: unknown; } : never;

export const useApiTask = (endpointName: EndpointFunctionNamesPrefixedByGet) => {
  const endpointHandler = endpoints[`get${endpointName}`];
}

  • Related