我正在尝试为适用于可区分联合的打字稿创建模式匹配函数。
例如:
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必须是一个关键Ttransforms必须是一个具有所有值的键的对象T[typeof tag]source必须是类型T换句话说,替换的类型决定了、和可以具有T的值。这对我来说似乎是最直接和最容易理解的,我将尝试给出一个示例实现。但在此之前,还有方法 2:tagtransformssource
如果您希望基于和 的source值在类型上有更大的灵活性,则可以在最后一次调用时给出或推断类型:tagtransforms
const match = (tag) => (transforms) => <T>(source) => ...
Run Code Online (Sandbox Code Playgroud)
在此示例中,T在最后一次调用时实例化,因此具有以下类型约束:
source必须有钥匙tagtypeof 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给定对象的确切形状。返回类型使用ReturnTypeTypeScript 标准库的帮助器来推断匹配函数的返回类型。实际的函数实现相当于你自己的例子:
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 次 |
| 最近记录: |