我正在尝试为适用于可区分联合的 typescript 创建模式匹配函数。
例如:
export type WatcherEvent =
| { code: "START" }
| {
code: "BUNDLE_END";
duration: number;
result: "good" | "bad";
}
| { code: "ERROR"; error: Error };
我希望能够输入 match
看起来像这样的函数:match("code")({
START: () => ({ type: "START" } as const),
ERROR: ({ error }) => ({ type: "ERROR", error }),
BUNDLE_END: ({ duration, result }) => ({
type: "UPDATE",
duration,
result
})
})({ code: "ERROR", error: new Error("foo") });
到目前为止我有这个export type NonTagType<A, K extends keyof A, Type extends string> = Omit<
Extract<A, { [k in K]: Type }>,
K
>;
type Matcher<Tag extends string, A extends { [k in Tag]: string }> = {
[K in A[Tag]]: (v: NonTagType<A, Tag, K>) => unknown;
};
export const match = <Tag extends string>(tag: Tag) => <
A extends { [k in Tag]: string }
>(
matcher: Matcher<Tag, A>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => <R extends any>(v: A): R =>
(matcher as any)[v[tag]](v);
但我不知道如何键入每个案例的返回类型目前每种情况都正确键入参数但返回类型未知
所以如果我们拿这个案子
ERROR: ({ error }) => ({ type: "ERROR", error }), // return type is not inferred presently
那么每个 case like function 的返回类型是未知的,因为 match
的返回类型也是未知的。函数本身:这是一个 codesandbox .
最佳答案
在我看来,您可以采用两种方法。
1.输入类型事先知道
如果要强制最终函数的初始化采用特定类型,则必须事先知道该类型:
// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...
在本例中,您指定 T
在第一次调用时,结果如下类型约束:tag
必须是 T
的 key transforms
必须是一个包含 T[typeof tag]
的所有值的键的对象source
必须是 T
类型换句话说,替代
T
的类型确定 tag
的值, transforms
和 source
可以有。这对我来说似乎是最简单易懂的,我将尝试为此提供一个示例实现。但在我这样做之前,还有方法2:2.输入类型是从上次调用推断的
如果您想在
source
的类型上有更多的灵活性基于 tag
的值和 transforms
,然后可以在最后一次调用时给出或推断出类型:const match = (tag) => (transforms) => <T>(source) => ...
在这个例子中 T
在上次调用时实例化,因此具有以下类型约束:source
必须有 key tag
typeof source[tag]
必须是最多所有 transforms
的联合键,即 keyof typeof transforms
.换句话说,(typeof source[tag]) extends (keyof typeof transforms)
对于给定的 source
必须始终为真. 这样,您就不会受限于
T
的特定替换。 ,但是 T
最终可能是满足上述约束的任何类型。这种方法的一个主要缺点是对 transforms
的类型检查很少。 ,因为它可以有任何形状。 tag
之间的兼容性, transforms
和 source
只能在最后一次调用后检查,这让事情变得更难理解,任何类型检查错误都可能相当神秘。因此,我将采用下面的第一种方法(而且,这个方法很难理解;)因为我们预先指定了类型,这将是第一个函数中的类型槽。为了与函数的其他部分兼容,它必须扩展
Record<string, any>
:const match = <T extends Record<string, any>>(tag: keyof T) => ...
我们为您的示例调用它的方式是:const result = match<WatcherEvent>('code') (...) (...)
We are going to need the type of
tag
for further building the function, but to parameterise that, e.g. withK
would result in an awkward API where you have to write the key literally twice:const match = <T extends Record<string, any>, K extends keyof T>(tag: K) const result = match<WatcherEvent, 'code'>('code') (...) (...)
So instead I'm going for a compromise where I'll write
typeof tag
instead ofK
further down the line.
接下来是采用
transforms
的函数,让我们使用类型参数 U
保持其类型:const match = <T extends Record<string, any>>(tag: keyof T) => (
<U extends ?>(transforms: U) => ...
)
U
的类型约束是它变得棘手的地方。所以U
对于 T[typeof tag]
的每个值必须是一个具有一个键的对象,每个键都包含一个转换 WatcherEvent
的函数任何你喜欢的东西( any
)。但不仅仅是任何 WatcherEvent
,特别是将相应的键作为 code
的值的那个.要输入这个,我们需要一个帮助类型来缩小 WatcherEvent
联合到一个成员。概括这种行为,我想出了以下几点:// If T extends an object of shape { K: V }, add it to the output:
type Matching<T, K extends keyof T, V> = T extends Record<K, V> ? T : never
// So that Matching<WatcherEvent, 'code', 'ERROR'> == { code: "ERROR"; error: Error }
使用这个助手我们可以编写第二个函数,并填写 U
的类型约束。如下:const match = <T extends Record<string, any>>(tag: keyof T) => (
<U extends { [V in T[typeof tag]]: (input: Matching<T, typeof tag, V>) => any }>(transforms: U) => ...
)
此约束将确保 transforms
中的所有函数输入签名拟合 T
的推断成员union (或 WatcherEvent
在您的示例中)。Note that the return type
any
here does not loosen the return type ultimately (because we can infer that later on). It simply means that you're free to return anything you want from functions intransforms
.
现在我们来到了最后一个函数——接受最后一个
source
的函数。 ,它的输入签名非常简单; S
必须延长 T
,其中 T
是 WatcherEvent
在您的示例中,和 S
将是准确的 const
给定对象的形状。返回类型使用 ReturnType
typescript 标准库的帮助器,用于推断匹配函数的返回类型。实际的函数实现相当于你自己的例子:const match = <T extends Record<string, any>>(tag: keyof T) => (
<U extends { [V in T[typeof tag]]: (input: Matching<T, typeof tag, V>) => any }>(transforms: U) => (
<S extends T>(source: S): ReturnType<U[S[typeof tag]]> => (
transforms[source[tag]](source)
)
)
)
应该是这样!现在我们可以拨打 match (...) (...)
获取函数 f
我们可以针对不同的输入进行测试:// Disobeying some common style rules for clarity here ;)
const f = match<WatcherEvent>("code") ({
START : () => ({ type: "START" }),
ERROR : ({ error }) => ({ type: "ERROR", error }),
BUNDLE_END : ({ duration, result }) => ({ type: "UPDATE", duration, result }),
})
并尝试使用不同的 WatcherEvent
成员给出以下结果:const x = f({ code: 'START' }) // { type: string; }
const y = f({ code: 'BUNDLE_END', duration: 100, result: 'good' }) // { type: string; duration: number; result: "good" | "bad"; }
const z = f({ code: "ERROR", error: new Error("foo") }) // { type: string; error: Error; }
注意,当你给 f
WatcherEvent
(联合类型)而不是文字值,返回的类型也将是转换中所有返回值的联合,这对我来说似乎是正确的行为:const input: WatcherEvent = { code: 'START' }
const output = f(input)
// typeof output == { type: string; }
// | { type: string; duration: number; result: "good" | "bad"; }
// | { type: string; error: Error; }
最后,如果您需要返回类型中的特定字符串文字而不是通用 string
类型,你可以通过改变你定义为 transforms
的函数来做到这一点。 .例如,您可以定义额外的联合类型,或使用“as const
” ' 函数实现中的注释。这是一个 TSPlayground link ,我希望这就是你要找的!
关于typescript - typescript 中模式匹配函数的返回类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64884948/