我做了一个match函数,它接受一个值和一个数组[case, func],将该值与每个case进行比较,如果找到匹配,则调用其关联的func,并将该值作为参数:

type Case<T> = [T | Array<T>, (value: T) => void];

const match = <V>(value: V, cases: Array<Case<V>>) => {
  for (const [key, func] of cases) {
    if (Array.isArray(key) ? key.includes(value) : key === value) {
      return func(value);
    }
  }
};

我想将每个func的值缩小到与其关联的case,即:

// const value: string = "whatever";
match(value, [
  [
    "something",
    (v) => {
      // here, `v` should narrow to `"something"`
    }
  ],
  [
    ["other", "thing"],
    (v) => {
      // here, `v` should narrow to `"other" | "thing"`
    }
  ]
] as const);

推荐答案

您可以为match()编写正确的类型,但它不会以您希望的方式推断回调参数类型.这是TypeScrip中缺失的功能.您要么需要等待该功能实现(可能永远不会实现),要么以某种方式重构,要么放弃.


正确输入match()如下所示:

const match = <V, C extends V[]>(
  value: V,
  cases: [...{ [I in keyof C]: Case<C[I]> }]
) => {
  for (const [key, func] of cases) {
    if (Array.isArray(key) ? key.includes(value) : key === value) {
      return func(value as never);
    }
  }
};

在这里,cases的类型不仅仅是Array<Case<V>>,它不会试图跟踪数组中每个元素上V的各个子类型,而且这对于子类型甚至是不正确的……例如,Case<"something">不能分配给Case<string>,因为虽然您可以将"something"分配给string,但由于function types are contravariant in their inputs的事实,您可以将(value: "something") => void分配给(value: string) => void.

相反,cases的类型是mapped array type,而不是新的generic类型参数C,新的generic类型参数Cconstrained至子类型V[].C的每个元素用Case<⋯>包装,以得到cases的对应元素.因此,如果由["x", "y", "z"]指定C,则cases的类型为[Case<"x">, Case<"y">, Case<"z">].


希望是如果你打电话给

const value: string = "whatever";
match(value, [
  [ "something", (v) => {} ],
  [ ["other", "thing"], (v) => { }]
]);

编译器会将V推断为string,这种情况确实会发生.它还会推断C等于[ "something", "other" | "thing" ],不幸的是,not确实发生了.即使发生了did次,这些回调中的v次中的contextual typing次也不会及时发生.

当函数参数相互依赖时,类型判断器很难推断出C这样的泛型类型参数,同时推断出c这样的函数参数的上下文类型.目前关于这一现象的公开问题一般是microsoft/TypeScript#47599个.特别是对于match(),最适用的问题可能是microsoft/TypeScript#53018,因为我们试图从映射类型推断C(从映射类型推断有时称为"反向"映射类型),然后在每个元素中上下文推断v.

目前这还没有实现,所以上面的调用最终无法正确地推断出C.它只是string[],这意味着v的两个实例都是类型string,这不是你想要的.


我不知道微软/TypeScrip#53018是否会实现,甚至不知道如果实现了,上面的代码是否会直接工作.目前,我们所能做的就是放弃或解决这个问题.

有很多可能的解决方法;最简单的一个可能是在对match()的调用中使用助手函数来一次推断一个 case ,而不是一次推断所有 case .大概是这样的:

match(value, [
  c("something", (v) => { }),
  c(["other", "thing"], (v) => { })
]);

其中,c被定义为

const c = <const T,>(
  value: T | Array<T>,
  handler: (value: T) => void
): Case<T> => [value, handler];

这是可行的;第一个v被推断为"something",第二个v被推断为"other" | "thing",因此,根据需要,对match()的调用中的C被推断为["something", "other"|"thing"].每次拨打c只需要担心推断一件事,而不是一系列事情.

是的,这只是一种变通方法,但它相当轻量级;您所需要做的就是将[xxx,yyy]替换为c(xxx,yyy),这只是一个额外的击键.是的,有一些认知开销,所以这就是为什么这是一个变通的办法,而不是直接的解决方案.

此解决方法或其他解决方法是否可接受取决于您的用例,并且由于问题不要求解决方法,因此这可能超出了问题的范围,所以我现在就不说了.

Playground link to code

Typescript相关问答推荐

带有微前端的动态React路由问题

如果存在ts—mock—imports,我们是否需要typescpt中的IoC容器

如何将联合文字类型限制为文字类型之一

如何缩小变量访问的对象属性范围?

在动态对话框中使用STEP组件实现布线

如何在Typescript 中输入两个相互关联的变量?

获取函数中具有动态泛型的函数的参数

打印脚本中的动态函数重载

如何判断对象是否有重叠的叶子并产生有用的错误消息

诡异的文字类型--S为物语的关键

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

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

如何将TS泛型用于react-hook-form接口?

在组件卸载时try 使用useEffect清除状态时发生冲突.react +打字

ANGLE NumberValueAccessor表单控件值更改订阅两次

如何从一个泛型类型推断多个类型

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

导航链接不触发自定义挂钩

数据库中的表请求返回如下日期:2023-07-20T21:39:00.000Z 我需要将此数据格式化为 dd/MM/yyyy HH:mm

剧作家测试未加载本地网址