假设我有以下代码:

const selectors = {
  posts: {
    getAllPosts(): Post[] {
      return database.findPosts();
    },
    getPostsWithStatus(status: Post["status"]) {
      return database.findPosts({ status });
    }
  },
  users: {
    getCurrentUser(): User {
      return database.findUser();
    }
  }
};

我想创建一个调用其中一个嵌套方法的函数,以便执行以下操作:

const deletedPosts = select("posts", "getPostsWithStatus", ["deleted"]);

到目前为止,我的实现如下:

type Selectors = typeof selectors;

function select<
  SelectorType extends keyof Selectors,
  SelectorFn extends keyof Selectors[SelectorType]
>(
  selectorType: SelectorType,
  selectorFn: SelectorFn,
  args: Parameters<Selectors[SelectorType][SelectorFn]>
): ReturnType<Selectors[SelectorType][SelectorFn]> {
  return selectors[selectorType][selectorFn](...args);
}

TypeScript Playground

调用函数时会正确推断类型,但实现本身会引发错误:

args: Parameters<Selectors[SelectorType][SelectorFn]>
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

键入{posts:{getAllPosts():Post[];getPostsWithStatus(状态:"已删除"|未定义):never[];};用户:{getCurrentUser():用户;};}[SelectorType][SelectorFn]'不满足约束'(…参数:any)=&gt;"任何".

): ReturnType<Selectors[SelectorType][SelectorFn]> {
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

键入{posts:{getAllPosts():Post[];getPostsWithStatus(状态:"已删除"|未定义):never[];};用户:{getCurrentUser():用户;};}[SelectorType][SelectorFn]'不满足约束'(…参数:any)=&gt;"任何".

return selectors[selectorType][selectorFn](...args);
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

此表达式不可调用.

所以我的问题是,如何创建一个函数,在保留类型的同时动态调用嵌套在对象中的另一个函数?

推荐答案

您遇到了TypeScript的一些限制和痛点,处理它的最好方法可能是绕过它们.


第一个是一个已知的bug,归档编号为microsoft/TypeScript#21760.编译器无法遵循键类型为generic的嵌套indexed accesses的约束.我不确定这是否或何时会得到解决.

所以,如果是K extends keyof SelectorsP extends keyof Selectors[K],那么编译器忘记了Selectors[K][P] extends (...args: any) => any,所以不能将其传递给the Parameters<T> utility typethe ReturnType<T> utility type,其类型参数是该函数类型的constrained.

相反,我们可以编写Parameters<T>ReturnType<T>的无约束版本.implementation of Parameters<T>就像T extends (...args: infer P) => any ? P : never,所以我们可以直接使用它(但我会将P改为A):

function select<
  K extends keyof Selectors,
  P extends keyof Selectors[K]
>(
  selectorType: K,
  selectorFn: P,
  args: Selectors[K][P] extends ((...args: infer A) => any) ? A : never
): Selectors[K][P] extends (...args: any) => infer R ? R : never {
  return selectors[selectorType][selectorFn](...args); // still error
}


const deletedPosts: Post[] = select("posts", "getPostsWithStatus", ["deleted"]);

现在,select()的调用签名没有错误,您仍然可以调用它.但是实现仍然有一个错误,这给我们带来了第二个痛点.


这是一种一般类型的限制,归档为microsoft/TypeScript#30581.selectors[selectorType][selectorFn]类型和args类型之间有correlation,但编译器无法遵循这一点.编译器无法对类型执行任意分析,并且它不知道如果F是泛型的,或者如果它是函数类型的union,那么可以像f(...a)一样调用函数f或类型F以及类型Parameters<F>的值a.编译器不会对每个可能的KP类型参数进行逐案分析.对于每个可能的select()调用,第selectors[selectorType][selectorFn](...args)行都是有效的,但编译器不会多次分析该行.它只执行一次,但由于过于复杂,因此不被视为类型安全.

microsoft/TypeScript#47109处有一个补丁,允许您重构代码,以将这些关联表示为某些基类型上的泛型操作.但我无法将其用于您的代码,可能是因为以前的嵌套泛型索引问题.

因此,我们不需要试图让编译器看到相关性,而只需要从编译器那里卸下责任,使用type assertion,如下所示:

return (selectors[selectorType][selectorFn] as any)(...args);

像这样使用the any type通常是过分的,但"正确"的断言是一个复杂的混乱,并不能真正提供任何类型的安全性.无论您断言什么,请记住类型断言是告诉编译器不要担心它无法理解的事情的一种方式...因此,你应该确信you已经找到了答案.在断言之前,反复判断selectors[selectorType][selectorFn](...args)是否始终安全.

Playground link to code

Typescript相关问答推荐

类型脚本类型字段组合

更新为Angular 17后的可选链运算符&?&问题

仅针对某些状态代码重试HTTP请求

有条件地删除区分的联合类型中的属性的可选属性

访问继承接口的属性时在html中出错.角形

通过字符串索引访问对象';S的值时出现文字脚本错误

为什么ESLint会抱怨函数调用返回的隐式`any`?

在打印脚本中使用泛型扩展抽象类

Typescript 类型守卫一个类泛型?

React router 6(数据路由)使用数据重定向

有没有办法取消分发元组的联合?

复选框Angular 在Ngfor中的其他块中重复

API文件夹内的工作员getAuth()帮助器返回:{UserID:空}

在Cypress中,您能找到表格中具有特定文本的第一行吗?

如何将类似数组的对象(字符串::匹配结果)转换为类型脚本中的数组?

编剧- Select 动态下拉选项

Typescript:是否将联合类型传递给重载函数?

两个接口的TypeScript并集,其中一个是可选的

使用本机 Promise 覆盖 WinJS Promise 作为 Chrome 扩展内容脚本?

const 使用条件类型时,通用类型参数不会推断为 const