繁体   English   中英

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

[英]return type for pattern matching function in 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函数本身的返回类型也是未知的:

这是一个代码沙盒

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

1.输入类型事先知道

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

// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...

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

  1. tag必须是T的键
  2. transforms必须是一个对象,其中包含T[typeof tag]所有值的键
  3. source必须是T类型

换句话说,替换T的类型决定了tagtransformssource可以具有的值。 这对我来说似乎是最简单易懂的,我将尝试为此提供一个示例实现。 但在我这样做之前,还有方法2:

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

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

const match = (tag) => (transforms) => <T>(source) => ...

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

  1. source必须有一个关键tag
  2. typeof source[tag]必须是最多所有transforms键的联合,即keyof typeof transforms 换句话说, (typeof source[tag]) extends (keyof typeof transforms)对于给定的source必须始终为真。

这样,您就不会受限于T的特定替换,但T最终可能是满足上述约束的任何类型。 这种方法的一个主要缺点是几乎没有对transforms进行类型检查,因为它可以具有任何形状。 tagtransformssource之间的兼容性只能在最后一次调用之后检查,这使得事情变得更加难以理解,并且任何类型检查错误都可能相当神秘。 因此,我将采用下面的第一种方法(而且,这个方法很难理解;)


因为我们预先指定了类型,这将是第一个函数中的类型槽。 为了与函数的其他部分兼容,它必须扩展Record<string, any>

const match = <T extends Record<string, any>>(tag: keyof T) => ...

我们为您的示例调用它的方式是:

const result = match<WatcherEvent>('code') (...) (...)

我们将需要tag的类型来进一步构建函数,但要参数化,例如使用K会导致尴尬的 API,您必须逐字写两次密钥:

 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) => ...
)

U的类型约束是棘手的地方。 所以U必须是一个对象,每个T[typeof tag]值都有一个键,每个键都包含一个函数,可以将WatcherEvent转换为您喜欢的any内容( 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工会(或WatcherEvent在你的例子)。

请注意,这里的返回类型any最终不会放松返回类型(因为我们稍后可以推断出这一点)。 它只是意味着您可以自由地从transforms函数返回任何您想要的东西。

现在我们来到了最后一个函数——接受最终source函数,它的输入签名非常简单; S必须扩展T ,其中T在您的示例中是WatcherEvent ,并且S将是给定对象的精确const形状。 返回类型使用ReturnType标准库的ReturnType助手来推断匹配函数的返回类型。 实际的函数实现相当于你自己的例子:

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

我认为这可以实现您想要的,但是我认为您尝试强制执行“NonTagType”是不可能的,我不确定为什么它甚至是可取的。

type Matcher<Types extends string = string, V = any> = {
  [K in Types]: (v: V) => unknown;
};

export const match = <Tag extends string>(tag: Tag) => <
  M extends Matcher
>(
  matcher: M
) => <V extends {[K in Tag]: keyof M }>(v: V) =>
  matcher[v[tag]](v) as ReturnType<M[V[Tag]]>;


const result = match("code")({
    START: () => ({ type: "START" } as const),
    ERROR: ({ error }: { error: Error }) => ({ type: "ERROR", error } as const),
    BUNDLE_END: ({ duration, result }: { duration: number, result: "good" | "bad" }) => ({
      type: "UPDATE",
      duration,
      result
    } as const)
})({ code: "ERROR", error: new Error("foo") })

这是一个非常密集的解决方案,除非我误解了它,否则它提供了您想要的一切:

export type WatcherEvent =
 | {
      code: 'START'
      //tag: 'S'
    }
  | {
      code: 'BUNDLE_END'
      // tag: 'B'
      duration: number
      result: 'good' | 'bad'
    }
  | {
      code: 'ERROR'
      // tag: 'E';
      error: Error
    }

const match = <U extends { [K in string]: any }>(u: U) => <
  T extends keyof Extract<U, { [K in keyof U]: string }> & keyof U
>(
  tag: T
) => <H extends { [K in U[T]]: (v: Omit<Extract<U, { [P in T]: K }>, T>) => any }>(h: H) =>
  h[u[tag]](u as any) as ReturnType<H[U[T]]>

const e: WatcherEvent = {} as any // unknown event
const e2: WatcherEvent = { code: 'ERROR', error: new Error('?') } // known event

// 'code' (or 'tag' if you uncomment tag:)
const result = match(e)('code')({
  // all START, BUNDLE_END and ERROR must be specified, but nothing more
  START: v => ({ type: 'S' as const }), // v is never; v.code was removed
  BUNDLE_END: v => ({
    type: 'BE' as const,
    dur: v.duration,
    result: v.result
  }),
  ERROR: () => ({ type: 'E' } as const)
})

result.type // 'E' | 'S' | 'BE'

const r2 = match(e2)('code')({
  // since e2 is a known event START and BUNDLE_END cannot be specified
  ERROR: () => null
})
r2 === null // true

上述解决方案的最大缺点是必须提前知道目标/数据才能推断其他类型(标签和匹配函数)。 可以通过明确指定目标类型来改进它,如下所示:

type Matcher<U extends { [K in string]: any }> = <
  T extends keyof Extract<U, { [K in keyof U]: string }> & keyof U
>(
  tag: T
) => <
  H extends { [K in U[T]]: (v: Omit<Extract<U, { [P in T]: K }>, T>) => any }
>(
  h: H
) => (u: U) => ReturnType<H[U[T]]>;

const matcher_any = <U extends { [K in string]: any }>(t: any) => (h: any) => (e: any) => h[e[t]](e as any) as Matcher<U>;
const createMatcher = <U extends { [K in string]: any }>() =>
  matcher_any as Matcher<U>;
const matcher = createMatcher<WatcherEvent>()("code")({
  // all START, BUNDLE_END and ERROR must be specified, but nothing more
  START: (v) => ({ type: "S" as const }), // v is never; v.code was removed
  BUNDLE_END: (v) => ({
    type: "BE" as const,
    dur: v.duration,
    result: v.result
  }),
  ERROR: () => ({ type: "E" } as const)
})
console.log(matcher(e).type)

代码沙盒

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM