打字稿中模式匹配函数的返回类型

dag*_*da1 7 typescript

我正在尝试为适用于可区分联合的打字稿创建模式匹配函数。

例如:

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函数本身的返回类型也是未知的:

这是一个代码沙盒

Jef*_*amp 3

在我看来,您可以采取两种方法。

1.输入类型事先已知

如果您想强制最终函数的初始化采用特定类型,则必须事先知道该类型:

// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...
Run Code Online (Sandbox Code Playgroud)

在此示例中,您在第一次调用时指定T,因此具有以下类型约束:

  1. tag必须是一个关键T
  2. transforms必须是一个具有所有值的键的对象T[typeof tag]
  3. source必须是类型T

换句话说,替换的类型决定了、和可以具有T的值。这对我来说似乎是最直接和最容易理解的,我将尝试给出一个示例实现。但在此之前,还有方法 2:tagtransformssource

2. 输入类型是从上次调用推断的

如果您希望基于和 的source值在类型上有更大的灵活性,则可以在最后一次调用时给出或推断类型:tagtransforms

const match = (tag) => (transforms) => <T>(source) => ...
Run Code Online (Sandbox Code Playgroud)

在此示例中,T在最后一次调用时实例化,因此具有以下类型约束:

  1. source必须有钥匙tag
  2. 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,您必须将密钥按字面意思写两次:

const match = <T extends Record<string, any>, K extends keyof T>(tag: K)
const result = match<WatcherEvent, 'code'>('code') (...) (...)
Run Code Online (Sandbox Code Playgroud)

因此,我会寻求一种妥协,我会写下来,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,在您的示例TWatcherEvent,并且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)

请注意,当您给出fWatcherEvent联合类型)而不是文字值时,返回的类型也将是转换中所有返回值的联合,这对我来说似乎是正确的行为:

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 链接,我希望这就是您正在寻找的!