繁体   English   中英

Typescript 为装饰器中间件模式键入

[英]Typescript typing for decorator middleware patterns

我正在考虑 Node 中间件(在各种框架中)。 通常中间件会在请求或响应 object 中添加一个属性,然后任何注册的中间件都可以使用该属性。

这个 model 的一个问题是你没有得到有效的打字。

为了演示,这里有一个高度简化的 model 这样的框架。 在这种情况下,一切都是同步的,每个中间件都会收到一个请求,并且还必须返回一个(可能会改变)。

interface Req {
    path: string;
    method: string;
}

type Middleware = (request: Req) => Req;

class App {

    mws: Middleware[] = [];

    use(mw: Middleware) {
        this.mws.push(mw);
    }

    run(): Req {

        let request = {
            path: '/foo',
            method: 'POST'
        }
        for(const mw of this.mws) {
           request = mw(request);
        }

        return request;

    }

}

为了演示应用程序,我定义了 2 个中间件。 第一个向Req添加一个新属性,第二个使用该属性:

const app = new App();

app.use( req => {
  req.foo = 'bar';
});

app.use( req => {
  console.log(req.foo);
}

Typescript 操场链接

这是服务器端 Javascript 框架中非常常见的模式。 不变这会引发错误:

Property 'foo' does not exist on type 'Req'.

对此的标准解决方案是使用接口声明合并:

interface Req {
    path: string;
    method: string;
}
interface Req {
    foo: string;
}

不过这也有缺点。 这全局性地改变了类型,我们实际上是在对 typescript 撒谎。 在调用第一个中间件之前,Req 类型将没有.foo属性。

所以,我认为这只是 Express、Koa、Curveball 中的“本来面目”,但我想知道如果我们编写一个新框架是否有可能采用不同的模式。

我认为不可能实现这个伪代码示例:

app.use(mw1); // Uses default signature
app.use(mw2); // Type of `req` is now the return type of `mw1`.

我认为链接可能是可能的:

app
  .use(mw1)
  .use(mw2);

但我很难完全掌握如何。 另外,如果有next function,它会变得更加复杂,但为了这个问题,让我们忽略它。

更广泛地说,我的问题是......当一个伟大的基于服务器端中间件的框架是专门为 Typescript 编写的时,它会是什么样子。 或者,也许还有其他更有效的模式,我只是太盲目了,看不到它。

好问题。 如果我站在你的立场上,我会创建一个扩展Request接口的新接口。

interface MyReq extends Request {
  foo: string;
}

getMovies(req: MyReq, res: Response): void {
  req.foo = "Hi there setting foo";
  res.json(this.movieService.getMovies());
}

对于像您这样的大型项目,这更具可持续性。 我很快在我打开的小项目上测试了它,它工作得很好,而且没有欺骗编译器:)。 看看它:

在此处输入图像描述

让我知道你的想法。 干杯。

有趣的问题,我没有完整的答案,但我认为有一些方法可以解决这个问题。

第一种方法

如果你定义的中间件更通用一点,你可以说它是一个简单的 function:

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,它接受许多中间件函数并返回最终中间件的结果。 您只需根据需要添加任意数量的中间件:

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)

这种方法的好处是,如果您的第二个中间件不能与您的第一个中间件的 output 一起使用,它会告诉您。 这个错误会很清楚。

除了必须单独写出每个中间件参数之外,缺点是 output 类型unknown 如果您事先不知道将注入多少个中间件,这很难解决。


第二种方法,“减少”中间件

因此,下一个问题可能是:是否可以确定 N 个中间件的“总”类型,组合起来。 回顾我们之前的中间件:

const m1 = (s: InitialContext) => s.path
const m2 = (s: string) => Number(s)
const m3 = (s: number) => `${s}${s}`

你可以说这 3 部分的组合类型是Middleware<InitialContext, string>因为第一个 function 有InitialContext作为输入,最后一个有string作为 output。

将它们与如下所示的 function 结合是可能的:

function reduceMiddleware<T extends Middleware<any, any>[]>(...middleware: T) {
  return middleware.reduce((a, b) => {
    return ((input: any) => b(a(input)))
  }) as any
}

我们可以给这个 function 一个 output 类型吗?

我们可以使用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) => DateMiddleware<string, Date>作为 output。

但是,我们需要组合的不是 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 中使用这个泛型:

function reduceMiddleware<T extends Middleware<any, any>[]>(...middleware: T): Combine<T> {
  return middleware.reduce((a, b) => {
    return ((input: any) => b(a(input)))
  }) as any
}

但是,请注意,在内部这个 function 不是类型安全的。 我们必须用 any 覆盖结果才能在没有编译器错误的情况下运行它。 但是,这样做可以让我们在代码的 rest 中具有类型安全性:

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

或者我们可以设置一些更大的应用程序,使用这个中间件来处理请求/响应周期。

请注意,尽管此计算类型将检查中间件的输入/输出是否匹配,但它不会真正给您任何错误消息。 它会给你未知的类型,当你使用组合的 function 时,它可能会给你一些类型错误。 没有任何类型提示可以帮助您解决这个问题,所以肯定有改进的方法。


所以回答你的问题:是的,有一些选项可以以更类型安全的方式使用中间件。 在 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>

暂无
暂无

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

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