带标记的联合的通用匹配函数的正确类型是什么?

时间:2019-07-30 23:07:33

标签: typescript

我想实现一个{rust}函数,该功能类似于Rust的match keyword,但是针对TypeScript的结构类型系统进行了调整。

可以(但不符合人体工程学)为每个需要匹配的未命名的带标记的联合显式编写函数的类型。当前,最好的选择似乎是切换大小写,必须将其包装在match中才能用作表达式。

(()=>{})()

一些所需的属性:

  1. 应验证每个可能的标签都具有相应的大小写功能。
  2. 应根据值的类型自动完成第二个参数中的标签名称。
  3. 不需要显式的通用参数。
  4. 更新:应正确地将返回的类型推断为所有可能类型的并集。

在理论上目前不可能实现这一点的情况下,我想知道为什么以及是否有一种正在进行的语言功能使之成为可能。

1 个答案:

答案 0 :(得分:2)

结合了映射的类型和条件(利用内置的Extract):

type Cases<V extends {tag: string}, R> = {
    [K in V['tag']]: (v: Extract<V, {tag: K}>) => R
}
function match<V extends {tag: string}, R>(value: V, cases: Cases<V, R>): R {
  return (cases as any)[value.tag](value);
}

此签名将为丢失的案例提供一个错误,推断出该案例的区分值类型(仅在首先键入=>时才在操场上自动完成),并在将丢失的案例指定为a时引发错误文字。

注意:由于分配了Shape,对于您的shape示例,它将推断出比const更强的类型。在那种情况下,match调用将引发有关额外情况circle的错误,但是提供类型明确地使之静音。 match<Shape, string>(shape, {...})

Link to playground

更新

要更好地推断返回类型,签名要复杂一些,但是可以做到:

type Cases<V extends {tag: string}> = {
    [K in V['tag']]: (v: Extract<V, {tag: K}>) => any
}
type OnlyKnownCases<C, T> = C & Record<Exclude<keyof C, T>, "Unknown case"> & Record<any, Function>

function match<V extends {tag: string}, C extends Cases<V> = Cases<V>>(value: V, cases: OnlyKnownCases<C, V['tag']>): ReturnType<C[V['tag']]> {
  return cases[value.tag](value);
}

引入泛型C可以根据参数指定返回类型,以便将其推迟到呼叫站点。不幸的是,由于任何extends Cases<V>都被允许,所以这会带来额外的情况。我们通过说任何不在有效标签(Exclude<keyof C, T>上)的情况来限制这一点,该情况必须映射到字符串"Unknown case"上,该字符串会比never产生更好的错误。为了完整起见,我添加了& Record<any, Function>,以便在非常奇怪的情况下,将case函数写为"Unknown case"时,它不会绕过我们的限制。

ReturnType<C['someTag']>给出someTag的case函数的返回类型。因此ReturnType<C[V['tag']]>将处理所有案例的并集,以提供案例函数中所有返回类型的并集。

Link to playground