布设

我有以下Typescript 类型:

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]>,
};

并希望将其转换为以下内容(是的,也是大写的变化):

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 {}

无论出于何种原因,它都会设法正确地获取返回类型,但不能正确获取参数.如果API键没有参数,try 使用任何参数只会导致错误,如果API键有任何参数,处理程序上的第一个参数将是Event,下面的参数将是never.所以就像这样:

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 };
});

更奇怪的是,如果我try 使用原始的API密钥名称,我会得到正确的参数,例如:

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 };
});

奇怪的是,如果我不大写name变量,一切似乎都按预期工作.

做这件事最简单的方法是什么?这是一种极端的极端情况,还是我错过了什么?

推荐答案

像这样的呼叫签名的问题是,

declare function handle<K extends keyof API>(name: Capitalize<K>, ⋯

是您希望编译器能够接受类型Captialize<K>的参数name,然后从它推断出K.你可以把generic推理看作是试图向后运行一个类型函数,从它的输出中找到它的输入.实际上,对于常见的用例,编译器在这方面做得相当好.不常见的版本并不总是受支持.一般来说,这是不可能的,或者在可能的情况下是不可行的.请参见How can I infer inputs to a function, given an output?.

编译器不try 从intrinsic Capitalize<T> utility type推断.相反,它只是try "也许T只是Capitalize<T>",并在大多数情况下失败(并导致您看到的"更奇怪"的行为).


与其强迫编译器为您"undo撤消"Capitalize,不如使推理尽可能简单. 最简单的可能推论是,你要反转的类型函数是identity function,形式为declare function handle<K extends ⋯>(name: K, ⋯. 第K章从自己开始

您所要做的似乎就是在类型like-API中查找name,但其中的键是大写的:

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]>;
} */

这里CapKeys<T>key-remapped type,CapKeys<API>是您要索引的类型.一旦你做到了这一点,一切都会顺利进行:

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 };
});

同样,我们不是试图从Capitalize<K>推断K extends keyof API,而是试图从K推断K extends keyof CapKeysAPI.这成功了,并且CapKeysAPI包含与KAPI相关的信息.

Playground link to code

Typescript相关问答推荐

如何从TypScript中的接口中正确获取特定键类型的所有属性?

返回同时具有固定键和变量键的对象的函数的返回类型

使Typescribe强制所有对象具有相同的 struct ,从两个选项

如何重构长联合类型?

有什么方法可以类型安全地包装import()函数吗?

使用数组作为类型中允许的键的列表

按函数名的类型安全类型脚本方法包装帮助器

保护函数调用,以便深度嵌套的对象具有必须与同级属性函数cargument的类型匹配的键

在类型脚本中将泛型字符串数组转换为字符串字母并集

将带有样式的Reaction组件导入到Pages文件夹时不起作用

是否可以通过映射类型将函数参数约束为预定义类型?

当数组类型被扩展并提供标准数组时,TypeScrip不会抱怨

为什么TS2339;TS2339;TS2339:类型A上不存在属性a?

在Google授权后由客户端接收令牌

为什么数字是`{[key:string]:UNKNOWN;}`的有效密钥?

我的类型谓词函数不能像我预期的那样工作.需要帮助了解原因

我可以使用JsDocs来澄清Typescript实用程序类型吗?

仅当定义了值时才按值筛选数组

为什么过滤器不改变可能的类型?

如何确定剧作家中给定定位器的值?