我正在尝试为适用于可区分联合的打字稿创建模式匹配函数。
例如:
export type WatcherEvent =
| { code: "START" }
| {
code: "BUNDLE_END";
duration: number;
result: "good" | "bad";
}
| { code: "ERROR"; error: Error };
Run Code Online (Sandbox Code Playgroud)
我希望能够键入一个如下所示的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") });
Run Code Online (Sandbox Code Playgroud)
到目前为止我有这个
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);
Run Code Online (Sandbox Code Playgroud)
但我不知道如何键入每个案例的返回类型
目前,每种情况都正确键入参数,但返回类型未知,因此如果我们采用这种情况
ERROR: ({ error }) => ({ type: "ERROR", error }), // return type is not inferred presently
Run Code Online (Sandbox Code Playgroud)
那么每个 case like function 的返回类型是未知的,match
函数本身的返回类型也是未知的:
这是一个代码沙盒。
在我看来,您可以采取两种方法。
如果您想强制最终函数的初始化采用特定类型,则必须事先知道该类型:
// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...
Run Code Online (Sandbox Code Playgroud)
在此示例中,您在第一次调用时指定T
,因此具有以下类型约束:
tag
必须是一个关键T
transforms
必须是一个具有所有值的键的对象T[typeof tag]
source
必须是类型T
换句话说,替换的类型决定了、和可以具有T
的值。这对我来说似乎是最直接和最容易理解的,我将尝试给出一个示例实现。但在此之前,还有方法 2:tag
transforms
source
如果您希望基于和 的source
值在类型上有更大的灵活性,则可以在最后一次调用时给出或推断类型:tag
transforms
const match = (tag) => (transforms) => <T>(source) => ...
Run Code Online (Sandbox Code Playgroud)
在此示例中,T
在最后一次调用时实例化,因此具有以下类型约束:
source
必须有钥匙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) => ...
Run Code Online (Sandbox Code Playgroud)
对于您的示例,我们的调用方式是:
const result = match<WatcherEvent>('code') (...) (...)
Run Code Online (Sandbox Code Playgroud)
我们将需要 的类型
tag
来进一步构建函数,但是要对其进行参数化,例如 withK
会导致一个尴尬的 API,您必须将密钥按字面意思写两次:Run Code Online (Sandbox Code Playgroud)const match = <T extends Record<string, any>, K extends keyof T>(tag: K) const result = match<WatcherEvent, 'code'>('code') (...) (...)
因此,我会寻求一种妥协,我会写下来,
typeof tag
而不是K
进一步深入。
接下来是采用 的函数transforms
,让我们使用类型参数U
来保存其类型:
const match = <T extends Record<string, any>>(tag: keyof T) => (
<U extends ?>(transforms: U) => ...
)
Run Code Online (Sandbox Code Playgroud)
的类型约束U
是棘手的地方。因此U
必须是一个对象,其中每个值都有一个键T[typeof tag]
,每个键都包含一个将 a 转换为您喜欢的任何值的函数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 }
Run Code Online (Sandbox Code Playgroud)
有了这个助手,我们可以编写第二个函数,并填写类型约束,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) => ...
)
Run Code Online (Sandbox Code Playgroud)
此约束将确保所有函数输入签名都适合transforms
联合的推断成员T
(或WatcherEvent
在您的示例中)。
请注意,这里的返回类型
any
最终不会放松返回类型(因为我们可以稍后推断)。它只是意味着您可以自由地从 中的函数返回任何您想要的内容transforms
。
现在我们来到了最后一个函数——接受final的函数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)
)
)
)
Run Code Online (Sandbox Code Playgroud)
应该是这样!现在我们可以调用以获得一个可以针对不同输入进行测试的match (...) (...)
函数:f
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)
)
)
)
Run Code Online (Sandbox Code Playgroud)
与不同的成员一起尝试会WatcherEvent
得到以下结果:
// 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 }),
})
Run Code Online (Sandbox Code Playgroud)
请注意,当您给出f
(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; }
Run Code Online (Sandbox Code Playgroud)
最后,如果您需要返回类型中的特定字符串文字而不是泛型string
类型,您可以通过更改定义为 的函数来实现transforms
。例如,您可以定义额外的联合类型,或as const
在函数实现中使用“ ”注释。
这是TSPlayground 链接,我希望这就是您正在寻找的!
归档时间: |
|
查看次数: |
250 次 |
最近记录: |