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];
}
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
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:
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}`];
}