有趣的问题,我没有完整的答案,但我认为有一些方法可以解决这个问题。
第一种方法
如果你想定义一个更通用的中间件,你可以说它是一个简单的函数:
type Middleware<In, Out> = (input: In) => Out
在 HTTP 环境中,您将使用某种包含请求数据作为输入的上下文:
interface InitialContext {
method: 'GET' | 'POST'
path: string
query?: Record<string, string>
headers?: Record<string, string>
}
我会说第一个中间件元素应该将InitialContext 转换为其他内容。如果你想要灵活,它可以是任何东西。更实际地,您可能想要扩展InitialContext。现在我只坚持中间件可以将InitialContext 转换为任何东西的想法,下一个中间件需要处理这个问题。
一种方法可能是只创建一个函数,该函数接受多个中间件函数并返回最终中间件的结果。您只需根据需要添加任意数量的中间件:
function requestHandler<
M1,
M2,
M3,
M4,
>(
context: InitialContext,
middleware1?: Middleware<InitialContext, M1>,
middleware2?: Middleware<M1, M2>,
middleware3?: Middleware<M2, M3>,
middleware4?: Middleware<M3, M4>,
) {
if (!middleware1) return context
if (!middleware2) return middleware1(context)
if (!middleware3) return middleware2(middleware1(context))
if (!middleware4) return middleware3(middleware2(middleware1(context)))
return middleware4(middleware3(middleware2(middleware1(context))))
}
这仅限于 4,但当然可以使用一些代码生成扩展到任何有限数。它有效:
const m1 = (s: InitialContext) => s.path
const m2 = (s: string) => Number(s)
const m3 = (s: number) => `${s}${s}`
const response = requestHandler({
method: 'GET',
path: '/blabla'
}, m1, m2, m3)
这种方法的好处是,如果您的第二个中间件不能与您第一个的输出一起工作,它会告诉您有关情况。而且这个错误会很明显。
除了必须单独写出每个中间件参数之外,缺点是输出类型为unknown。如果事先不知道要注入多少个中间件,这很难解决。
第二种方法,“减少”中间件
所以,下一个问题可能是:是否可以确定 N 个中间件的“总”类型,组合起来。回顾我们之前的中间件:
const m1 = (s: InitialContext) => s.path
const m2 = (s: string) => Number(s)
const m3 = (s: number) => `${s}${s}`
您可以说这 3 个部分的组合类型是 Middleware<InitialContext, string>,因为第一个函数以 InitialContext 作为输入,最后一个函数以 string 作为输出。
将它们与如下所示的函数结合是可能的:
function reduceMiddleware<T extends Middleware<any, any>[]>(...middleware: T) {
return middleware.reduce((a, b) => {
return ((input: any) => b(a(input)))
}) as any
}
我们可以给这个函数一个输出类型吗?
我们可以使用infer 和条件类型将两种中间件类型组合在一起:
type TwoIntoOne<T> = T extends [Middleware<infer A, any>, Middleware<any, infer B>] ? Middleware<A, B> : unknown
使用以下方法对此进行测试:
type Combined = TwoIntoOne<[Middleware<string, number>, Middleware<number, Date>]>
会给我们(input: string) => Date 或Middleware<string, Date> 作为输出。
但是,我们需要组合的不是 2 个而是 N 个中间件。这意味着我们需要某种循环,或者我们一起减少类型。减少可以使用递归来实现,而递归是 Typescript 类型所支持的!
所以,我们可以使用递归来创建某种类型,将 N 个类型组合成一个类型:
type Combine<T> = T extends [Middleware<infer Ain, infer Aout>, Middleware<infer Aout, infer Bout>, ... infer Rest] ?
Combine<[Middleware<Ain, Bout>, ...Rest]> : T
这个最终类型将被包裹在一个数组中,我们可以做一个简单的调整来解开它:
type Unwrap<T> = T extends [infer A] ? A : unknown
type Combine<T> = T extends [Middleware<infer Ain, infer Aout>, Middleware<infer Aout, infer Bout>, ... infer Rest] ?
Combine<[Middleware<Ain, Bout>, ...Rest]> :
Unwrap<T>
我们可以在我们之前创建的函数中使用这个泛型:
function reduceMiddleware<T extends Middleware<any, any>[]>(...middleware: T): Combine<T> {
return middleware.reduce((a, b) => {
return ((input: any) => b(a(input)))
}) as any
}
但是,请注意,此函数在内部不是类型安全的。我们必须用 any 覆盖结果才能在没有编译器错误的情况下运行它。但是,这样做可以让我们在其余代码中具有类型安全性:
const m1 = (s: InitialContext) => s.path
const m2 = (s: string) => Number(s)
const m3 = (s: number) => `${s}${s}`
const combined = reduceMiddleware(
m1, m2, m3
)
// Calculated type:
// const combined: Middleware<InitialContext, string>
我们可以简单地使用这个中间件直接使用一些上下文作为输入:
const output = combined({
method: 'GET',
path: '/blabla'
})
或者我们可以设置一些更大的应用程序,使用这个中间件来处理请求/响应周期。
请注意,虽然这种计算类型会检查中间件的输入/输出是否匹配,但它不会真正给您任何错误消息。它会给你未知的类型,当你使用组合函数时,它可能会给你一些类型错误。没有任何类型提示可以帮助您解决这个问题,所以肯定有改进的方法。
所以回答你的问题:是的,有一些选项可以以更安全的方式使用中间件。在 OOP 方法中将其结合起来可能会很棘手,但使用单个中间件而不是 N 已经使这变得更加可行。
一种非常简单的方法是在构造函数中接受一个中间件,并期望用户将中间件 reducer 作为一个单独的步骤使用:
const combined = reduceMiddleware(
m1, m2, m3
)
class App<In, Out> {
middleware: Middleware<In, Out>
constructor(middleware: Middleware<In, Out>) {
this.middleware = middleware
}
}
const app = new App(combined)
// Calculated type:
// const app: App<InitialContext, string>