[英]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
函数本身的返回类型也是未知的:
这是一个代码沙盒。
在我看来,您可以采用两种方法。
如果要强制最终函数的初始化采用特定类型,则必须事先知道该类型:
// Other types omitted for clarity:
const match = <T>(tag) => (transforms) => (source) => ...
在此示例中,您在第一次调用时指定T
,因此具有以下类型约束:
tag
必须是T
的键transforms
必须是一个对象,其中包含T[typeof tag]
所有值的键source
必须是T
类型换句话说,替换T
的类型决定了tag
、 transforms
和source
可以具有的值。 这对我来说似乎是最简单易懂的,我将尝试为此提供一个示例实现。 但在我这样做之前,还有方法2:
如果您希望根据tag
和transforms
的值在source
类型中具有更大的灵活性,则可以在最后一次调用时给出或推断出类型:
const match = (tag) => (transforms) => <T>(source) => ...
在此示例中, T
在上次调用时被实例化,因此具有以下类型约束:
source
必须有一个关键tag
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) => ...
我们为您的示例调用它的方式是:
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.