[英]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.在我看来,您可以采用两种方法。
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
,因此具有以下类型约束:
tag
must be a key of T
tag
必须是T
的键transforms
must be an object with keys for all values of T[typeof tag]
transforms
必须是一个对象,其中包含T[typeof tag]
所有值的键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
的类型决定了tag
、 transforms
和source
可以具有的值。 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:
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:如果您希望根据
tag
和transforms
的值在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
在上次调用时被实例化,因此具有以下类型约束:
source
must have a key tag
source
必须有一个关键tag
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. tag
、 transforms
和source
之间的兼容性只能在最后一次调用之后检查,这使得事情变得更加难以理解,并且任何类型检查错误都可能相当神秘。 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 withK
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 ofK
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 intransforms
.它只是意味着您可以自由地从
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)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.