I have the following Typescript type:
type Item = { id: string };
type Config = { enabled: boolean };
type API = {
getItems(): Promise<Item[]> | undefined,
getItem(id: Item['id']): Promise<Item> | undefined,
getConfig(): Promise<Config>,
getConfigKey(key: keyof Config): Promise<Config[keyof Config]>,
};
and want to convert it to something like the following (yes the change of the capitalization too):
handle('GetItems', (event) => {
return [{} as Item]; // needs to follow the type above (Item[])
});
handle('GetItem', (event, id) => { // the parameter comes from the type above (has the type of Item['id'])
return { id } as Item;
});
handle('GetConfig', (event, item) => ({}) as Config); // this should error as no parameter is specified for the getConfig type above
function handle<T extends keyof API>(
name: Capitalize<T>,
handler: API[T] extends (...args: infer Args) => infer R
? (event: Event, ...args: Args) => R
: never
): void {}
This manages to get the return type correctly but not the parameters for whatever reason. If the API key had no parameters trying to use any parameter will just result in an error, if the API key had any parameters the first parameter on the handler would be Event
and the following parameters would be never
. So like this:
handle('GetConfig', async (event) => {
// ^
// Argument of type '(event: Event) => Promise<{ enabled: false; }>' is not assignable to parameter of type '(event: Event, ...args: never) => Promise<Item[]> | Promise<Item> | Promise<Config> | Promise<boolean> | undefined'.
// Types of parameters 'event' and 'event' are incompatible.
// Type '[event: Event, ...args: any[]]' is not assignable to type '[event: Event]'.
// Target allows only 1 element(s) but source may have more.ts(2345)
return { enabled: true };
});
handle('GetItem', async (event, id) => { // id has the type of never
return { id };
});
Something even more bizarre is that if I try to use the original API key name I get the correct parameters, for example:
handle('getItem', async (event, id) => { // event and id have the correct type but I get an error due to using 'getItem' instead of 'GetItem'
return { id };
});
Oddly enough if I don't capitalize the name
variable everything seems to work as expected.
What would be the simplest way of doing this? Is this an extreme edge case or am I missing something?
The problem with a call signature like
declare function handle<K extends keyof API>(name: Capitalize<K>, ⋯
is that you expect the compiler to be able to take an argument name
of type Captialize<K>
and then infer K
from it. You can think of generic inference as trying to run a type function "backwards", to find its input from its output. The compiler actually does a fairly good job of this in practice, for common use cases. Uncommon ones aren't always supported. And in general it's just not possible, or infeasible where possible. See How can I infer inputs to a function, given an output?.
The compiler makes no attempt to infer from the intrinsic Capitalize<T>
utility type. Instead it just tries "maybe T
is just Capitalize<T>
" and fails for most situations (and leads to the "even more bizarre" behavior you saw).
Instead of trying to force the compiler to "undo" Capitalize
for you, it would be better to make the inference as simple as possible. The simplest possible inference is where the type function you are inverting is the identity function, of the form declare function handle<K extends ⋯>(name: K, ⋯
. That is, infer K
from itself.
It seems like all you're doing is looking up name
in a type like API
, but where the keys have been capitalized:
type CapKeys<T> = { [K in keyof T as Capitalize<K & string>]: T[K] }
type CapKeysAPI = CapKeys<API>;
/* type CapKeysAPI = {
GetItems: () => Promise<Item[]> | undefined;
GetItem: (id: Item['id']) => Promise<Item> | undefined;
GetConfig: () => Promise<Config>;
GetConfigKey: (key: keyof Config) => Promise<Config[keyof Config]>;
} */
Here CapKeys<T>
is a key-remapped type and CapKeys<API>
is the type you're looking to index. Once you have that everything proceeds smoothly:
function handle<K extends keyof CapKeysAPI>(
name: K,
handler: CapKeysAPI[K] extends (...args: infer Args) => infer R
? (event: Event, ...args: Args) => R
: never
): void { }
handle('GetConfig', async (event) => { // okay
return { enabled: true };
});
handle('GetItem', async (event, id) => {
// ^?(parameter) id: string
return { id };
});
Again, instead of trying to infer K extends keyof API
from Capitalize<K>
, we are trying to infer K extends keyof CapKeysAPI
from K
. This succeeds, and CapKeysAPI
contains the information that relates K
to API
.