我正在开发一个事件驱动的应用程序框架,该框架利用中间件模式来处理从我的服务分派到第三方应用程序的事件。我的服务对第三方应用程序可能希望注册和接收的许多不同类型的事件进行建模。
当框架接收到事件有效负载时,它们会通过特定于中间件的附加功能进行“增强”,例如
next()
函数和用户可定义的 context
- 相对标准的中间件内容。然而,某些事件具有仅在特定事件上可用的进一步特定增强。例如,消息事件具有 message
属性,foo 事件可能具有 foo
属性,等等 - 尽管事件类型/名称和特定于事件的增强可能无法完美地一对一映射和/或它们可能有多种增强。
这些特定于事件的增强目前在应用程序框架中键入的方式存在问题,并且经常导致错误;因此,代码库大量使用类型断言(
as
)——这可能是一个坏兆头。诚然,我不是 TypeScript 专家,框架代码早于我参与;我只是想“我猜这就是 TS 项目的构建方式。”经过几年对 TypeScript 的了解,我想现在一定有更好的方法!
Here 是示例的 TypeScript 游乐场,我也在下面复制了它。
wrapMiddleware
方法错误:
Argument of type 'MiddlewareArgs<"message">' is not assignable to parameter of type 'MiddlewareArgs<string>'.
Types of property 'message' are incompatible.
Type 'MsgEvent' is not assignable to type 'undefined'.
import { expectAssignable } from 'tsd';
// A couple of events, and a union of all events (there are more in actuality)
interface MsgEvent {
type: 'message';
text: string;
channel: string;
user: string;
}
interface JoinEvent {
type: 'join';
channel: string;
user: string;
}
type AllEvents = MsgEvent | JoinEvent;
// Utility types to 'extract' event payloads out based on the `type` property
type KnownEventFromType<T extends string> = Extract<AllEvents, { type: T }>;
type EventFromType<T extends string> = KnownEventFromType<T> extends never ? { type: T } : KnownEventFromType<T>;
// A contrived example of arguments passed to middleware
interface MiddlewareArgs<EventType extends string = string> {
event: EventFromType<EventType>;
message: EventType extends 'message' ? this['event'] : undefined; // <-- problematic; there must be a better way, right?
}
// Augmenting events with additional middleware things
type AllMiddlewareArgs = {
next: () => Promise<void>;
}
function wrapMiddleware<Args extends MiddlewareArgs>(
args: Args,
): Args & AllMiddlewareArgs {
return {
...args,
next: async () => {},
}
}
// And now for the actual example:
const messageEvt: MsgEvent = {
type: 'message',
channel: 'random',
user: 'me',
text: 'hello world',
}
const messageEvtArgs: MiddlewareArgs<'message'> = {
event: messageEvt,
message: messageEvt,
}
const joinEvt: JoinEvent = {
type: 'join',
channel: 'random',
user: 'me'
}
const joinEvtArgs: MiddlewareArgs<'join'> = {
event: joinEvt,
message: undefined, // <-- bonus points if we can get rid of having to set an undefined message!
}
// Some test cases
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(messageEvtArgs));
expectAssignable<MiddlewareArgs<'message'>>(wrapMiddleware(messageEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(joinEvtArgs));
expectAssignable<MiddlewareArgs<'join'>>(wrapMiddleware(joinEvtArgs));
// Wrapping random untyped events should fallback
expectAssignable<AllMiddlewareArgs>(wrapMiddleware({ event: { type: 'garbage' }}));
expectAssignable<MiddlewareArgs<'garbage'>>(wrapMiddleware({ event: { type: 'garbage' }}));
我明白为什么我得到了我所做的错误:
wrapMiddleware
方法接受更广泛的、非message
特定的MiddlewareArgs类型,它将通用参数设置为string
,因此接口的message
属性根据 undefined
属性的条件类型,可以是消息对象或 message
。我的问题是:有什么更好的方法来构建这种方法,以便它可以扩展到更多事件,并具有不同的特定于事件的中间件参数形状?
要通过所有测试,最简单的方法是允许 wrapMiddleware()
接受
any对象输入:
function wrapMiddleware<A extends object>(a: A): A & AllMiddlewareArgs {
return {
...a,
next: async () => { },
}
}
// all okay
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(messageEvtArgs));
expectAssignable<MiddlewareArgs<'message'>>(wrapMiddleware(messageEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(joinEvtArgs));
expectAssignable<MiddlewareArgs<'join'>>(wrapMiddleware(joinEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware({ event: { type: 'garbage' } }));
expectAssignable<MiddlewareArgs<'garbage'>>(wrapMiddleware({ event: { type: 'garbage' } }));
这是因为,在基本层面上,所有
wrapMiddleware
所做的就是将输入传播到一个新对象中,并添加了 write 类型的
next
方法。 如果您只关心此类测试的通过,那么您应该这样做。就这么简单。
您执行其他操作的唯一原因是您想以某种方式无法匹配某些带外约束的输入:
function wrapMiddleware<A extends { event: { type: string } }>(
a: A & MiddlewareArgs<A["event"]["type"]>
): A & AllMiddlewareArgs {
return {
...a,
next: async () => { },
}
}
现在我们只接受
a
,其类型
A
是其 string
属性中具有 type
值的 event
属性,此外,仅接受可分配给 MiddlewareArgs<A["event"]["type"]>
的东西。这是一种验证,以确保已知类型具有您关心的属性。您基本上可以单独保留您的定义,但是允许 message
缺失并且不是未定义的重要一点,除非 type
是 "message"
是将您的 MiddlewareArgs
更改为将 条件类型置于更高级别,因此需要或不需要
{ message: MsgEvent }
:type MiddlewareArgs<K extends string = string> = {
event: EventFromType<K>;
} & (K extends "message" ? { message: MsgEvent } : unknown)
请注意,
unknown
类型
是交叉点的身份元素,因此
{event: EventFromType<K>} & unknown
只是{event: EventFromType<K>}
。您现有的测试仍然可以通过,但现在可能会失败,例如:
wrapMiddleware({ event: { type: "message", channel: "", text: "", user: "" } }); // error!
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Property 'message' is missing
wrapMiddleware({ event: { type: "join", user: "" } }); // error!
// ~~~~~
// Property 'channel' is missing