typescript - typescript 中模式匹配函数的返回类型

标签 typescript

我正在尝试为适用于可区分联合的 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 的值, transformssource可以有。这对我来说似乎是最简单易懂的,我将尝试为此提供一个示例实现。但在我这样做之前,还有方法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之间的兼容性, transformssource只能在最后一次调用后检查,这让事情变得更难理解,任何类型检查错误都可能相当神秘。因此,我将采用下面的第一种方法(而且,这个方法很难理解;)

    因为我们预先指定了类型,这将是第一个函数中的类型槽。为了与函数的其他部分兼容,它必须扩展 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. with K 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 of K 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 in transforms.


    现在我们来到了最后一个函数——接受最后一个 source 的函数。 ,它的输入签名非常简单; S必须延长 T ,其中 TWatcherEvent在您的示例中,和 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/

    相关文章:

    javascript - Typescript 和 Jquery 语法问题

    javascript - Angular - 使用可观察对象从静态 Web 服务中删除数据

    angular - 在其他提供程序中使用 APP_INITIALIZER

    typescript - Jest (ESM) 在单元测试中从 React Native 加载文件时出现问题

    javascript - 绑定(bind)元素 'x' 隐式具有 'any' 类型

    javascript - 如何处理 Angular 5 递归未知确切数字路由器参数?

    reactjs - TypeScript 打字问题(与 (P)React 略有相关)

    typescript - 在 AWS Lambda 和 AWS Cloudwatch 中使用 Typescript 源映射

    Angular 6 - @types/googlemaps/index.d.ts' 不是模块

    typescript - 如何在 typeorm 中组合 3 列,postgres 是唯一的?