简体   繁体   English

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

[英]return type for pattern matching function in typescript

I am trying to create a pattern-matching function for typescript that works on a discriminated union.我正在尝试为适用于可区分联合的打字稿创建模式匹配函数。

For example:例如:

export type WatcherEvent =
  | { code: "START" }
  | {
      code: "BUNDLE_END";
      duration: number;
      result: "good" | "bad";
    }
  | { code: "ERROR"; error: Error };

I want to be able to type a match function that looks like this:我希望能够输入如下所示的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") });

So far I have this到目前为止我有这个

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);

But I do not know how to type the return type of each case但我不知道如何键入每个案例的返回类型

At the moment each case is typing the parameters correctly but the return type is unknown so if we take this case目前,每种情况都正确键入参数,但返回类型未知,因此如果我们采用这种情况

ERROR: ({ error }) => ({ type: "ERROR", error }), // return type is not inferred presently

then the return type of each case like function is unknown as is the return type of match function itself:那么每个 case like function 的返回类型是未知的, match函数本身的返回类型也是未知的:

Here is a codesandbox .这是一个代码沙盒

The way I see it there are two approaches that you can take with this.在我看来,您可以采用两种方法。

1. The input type is known beforehand 1.输入类型事先知道

If you want to enforce that the initialisation of the final function takes a particular type then that type must be known beforehand:如果要强制最终函数的初始化采用特定类型,则必须事先知道该类型:

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

In this example you specify T at the time of the first call, with the following type constraints as a consequence:在此示例中,您在第一次调用时指定T ,因此具有以下类型约束:

  1. tag must be a key of T tag必须是T的键
  2. transforms must be an object with keys for all values of T[typeof tag] transforms必须是一个对象,其中包含T[typeof tag]所有值的键
  3. source must be of type T source必须是T类型

In other words, the type that substitutes T determines the values that tag , transforms and source can have.换句话说,替换T的类型决定了tagtransformssource可以具有的值。 This seems the most straightforward and understandable to me, and I'm going to try to give an example implementation for this.这对我来说似乎是最简单易懂的,我将尝试为此提供一个示例实现。 But before I do, there's also approach 2:但在我这样做之前,还有方法2:

2. the input type is inferred from the last call 2.输入类型是从上次调用推断的

If you want to have more flexibility in the type for source based on the values for tag and transforms , then the type can be given at, or inferred from, the last call:如果您希望根据tagtransforms的值在source类型中具有更大的灵活性,则可以在最后一次调用时给出或推断出类型:

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

In this example T is instantiated at the time of the last call, with the following type constraints as a consequence:在此示例中, T在上次调用时被实例化,因此具有以下类型约束:

  1. source must have a key tag source必须有一个关键tag
  2. typeof source[tag] must be a union of at most all transforms keys, ie keyof typeof transforms . typeof source[tag]必须是最多所有transforms键的联合,即keyof typeof transforms In other words, (typeof source[tag]) extends (keyof typeof transforms) must always be true for a given source .换句话说, (typeof source[tag]) extends (keyof typeof transforms)对于给定的source必须始终为真。

This way, you are not constrained to a specific substitution of T , but T might ultimately be any type that satisfies the above constraints.这样,您就不会受限于T的特定替换,但T最终可能是满足上述约束的任何类型。 A major downside to this approach is that there will be little type checking for the transforms , since that can have any shape.这种方法的一个主要缺点是几乎没有对transforms进行类型检查,因为它可以具有任何形状。 Compatibility between tag , transforms and source can only be checked after the last call, which makes things a lot harder to understand, and any type-checking errors will probably be rather cryptic. tagtransformssource之间的兼容性只能在最后一次调用之后检查,这使得事情变得更加难以理解,并且任何类型检查错误都可能相当神秘。 Therefore I'm going for the first approach below (also, this one is pretty tough to wrap my head around ;)因此,我将采用下面的第一种方法(而且,这个方法很难理解;)


Because we specify the type in advance, that's going to be a type slot in the first function.因为我们预先指定了类型,这将是第一个函数中的类型槽。 For compatibility with the further parts of the function it must extend Record<string, any> :为了与函数的其他部分兼容,它必须扩展Record<string, any>

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

The way we would call this for your example is:我们为您的示例调用它的方式是:

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

We are going to need the type of tag for further building the function, but to parameterise that, eg with K would result in an awkward API where you have to write the key literally twice:我们将需要tag的类型来进一步构建函数,但要参数化,例如使用K会导致尴尬的 API,您必须逐字写两次密钥:

 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.因此,相反,我将寻求妥协,我将进一步编写typeof tag而不是K

Next up is the function that takes the transforms , let's use the type parameter U to hold its type:接下来是接受transforms的函数,让我们使用类型参数U来保存它的类型:

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

The type constraint for U is where it gets tricky. U的类型约束是棘手的地方。 So U must be an object with one key for each value of T[typeof tag] , each key holding a function that transforms a WatcherEvent to anything you like ( any ).所以U必须是一个对象,每个T[typeof tag]值都有一个键,每个键都包含一个函数,可以将WatcherEvent转换为您喜欢的any内容( any )。 But not just any WatcherEvent , specifically the one that has the respective key as its value for code .但不仅仅是任何WatcherEvent ,特别是将相应的键作为code值的那个。 To type this we'll need a helper type that narrows down the WatcherEvent union to one single member.为了输入这个,我们需要一个辅助类型,将WatcherEvent联合缩小到一个成员。 Generalising this behaviour I came up with the following:概括这种行为,我想出了以下几点:

// 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 }

With this helper we can write the second function, and fill in the type constraint for U as follows:使用这个助手我们可以编写第二个函数,并填写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) => ...
)

This constraint will make sure that all function input signatures in transforms fit the inferred member of the T union (or WatcherEvent in your example).这种约束将确保在所有的函数输入签名transforms适合的推断成员T工会(或WatcherEvent在你的例子)。

Note that the return type any here does not loosen the return type ultimately (because we can infer that later on).请注意,这里的返回类型any最终不会放松返回类型(因为我们稍后可以推断出这一点)。 It simply means that you're free to return anything you want from functions in transforms .它只是意味着您可以自由地从transforms函数返回任何您想要的东西。

Now we've come to the last function -- the one that takes the final source , and its input signature is pretty straightforward;现在我们来到了最后一个函数——接受最终source函数,它的输入签名非常简单; S must extend T , where T was WatcherEvent in your example, and S is going to be the exact const shape of the given object. S必须扩展T ,其中T在您的示例中是WatcherEvent ,并且S将是给定对象的精确const形状。 The return type uses the ReturnType helper of typescript's standard library to infer the return type of the matching function.返回类型使用ReturnType标准库的ReturnType助手来推断匹配函数的返回类型。 The actual function implementation is equivalent to your own example:实际的函数实现相当于你自己的例子:

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

That should be it!应该是这样! Now we could call match (...) (...) to obtain a function f that we can test against different inputs:现在我们可以调用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 }),
})

And giving this a try with the different WatcherEvent members gives the following 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; }

Note that when you give f a WatcherEvent (union type) instead of a literal value, the returned type will also be the union of all return values in transforms, which seems like the proper behaviour to me:请注意,当您给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; }

Lastly, if you need specific string literals in the return types instead of the generic string type, you can do that by just altering the functions that you define as transforms .最后,如果您需要返回类型中的特定字符串文字而不是通用string类型,您只需更改您定义为transforms的函数即可。 For example you could define an additional union type, or use ' as const ' annotations in the function implementations.例如,您可以定义额外的联合类型,或在函数实现中使用“ as const ”注释。

Here's a TSPlayground link , I hope this is what you're looking for!这是TSPlayground 链接,我希望这是您要找的!

I think this accomplishes what you want, however I don't think your attempt at enforcing "NonTagType"s is possible and I'm not sure why it would even be desirable.我认为这可以实现您想要的,但是我认为您尝试强制执行“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") })

Here is a pretty dense solution, which - unless I misunderstood - provides everything you want:这是一个非常密集的解决方案,除非我误解了它,否则它提供了您想要的一切:

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

The biggest downside of the above solution is that target/data must be known in advance to infer other types (tag, and matching functions).上述解决方案的最大缺点是必须提前知道目标/数据才能推断其他类型(标签和匹配函数)。 It could be improved by explicitly specifying the target type, like so:可以通过明确指定目标类型来改进它,如下所示:

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)

codesandbox 代码沙盒

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

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