我有一个后端和前端之间的共享类型系统,在控制器中看起来像这样:
@Controller()
export class MyController {
@ApplyRoute('GET /foo')
async findAll(
@Query() { name }: Routes['GET /foo']['query'], // repeated route name
): Promise<Routes['GET /foo']['response']> { // repeated route name
return { foo: true }
}
}
注意'GET/foo'出现了3次:一次用来声明http方法/路径,一次用来获取查询参数的类型,还有一次用来强制返回类型。
看到操场
我正在寻找一种方法以某种方式删除重复,但我似乎想不出任何方法可以与装饰者玩得很好。现在有人可能使用一条路线的路径,然后使用另一条路线的query
或response
,这可能是非常错误的。
如何删除路由名的重复?
例如,做这个完全无效语法所暗示的事情:
@Controller()
export class MyController {
@ApplyRoute('GET /foo') {
async findAll(
@Query() { name }: CurrentRoute['query'],
): Promise<CurrentRoute['response']> {
return { foo: true }
}
}
}
对于额外的信息,管道工作如下:
有一个类型定义了所有路由,以及路由接受和返回的类型:
type Routes = {
'GET /foo': {
query: { name: string }
response: { foo: true }
}
'GET /bar': {
query: {},
response: { bar: true }
}
}
然后我们有一个小的辅助装饰器,它将路由名称解析为@RequestMapping()
装饰器,以设置控制器方法的http方法和路径:
export function ApplyRoute(routeName: keyof Routes): MethodDecorator {
const [method, path] = routeName.split(' ') as ['GET' | 'POST', string]
return applyDecorators(
RequestMapping({ path, method: RequestMethod[method] }),
)
}
可以使用动态路由技术来删除代码重复。考虑这个例子:
// define a controller that will host all methods of `Routes`
// it's, of course, also possible to have many of these or even have them generated based on the `Routes` descriptor
@Controller()
export class MyController {}
function Action<T extends keyof Routes>(route: T, fn: (query: Routes[T]["query"]) => Routes[T]["response"]): void {
const [method, path] = route.split(" ") as ["GET" | "POST", string];
const key = path.split("/").join("_");
// assigning the function body to a method in the controller class
MyController.prototype[key] = fn;
// applying the `Query` directive to the first parameter
// this could also be configured through the `Routes` in case if you have e.g. post methods
Query()(MyController.prototype, key, 0);
// applying the `Get` decorator to the controller method
RequestMapping({ path: path, method: RequestMethod[method] })(
MyController,
key,
Object.getOwnPropertyDescriptor(MyController.prototype, key)
);
}
// now, register the methods in the global scope
Action("GET /foo", ({ name: string }) => ({ foo: true }));
Action("GET /bar", () => ({ bar: true }));
不幸的是,decorator不能改变一个类的定义,而且在Typescript的repo中有很多关于decorator不能修改类及其定义的问题。据我所知,这包括类型定义,所以没有直接的办法说,当一个方法有装饰符时,返回的应该是不同的类型。以您的人为示例为例,您可以得到的最接近的方法是将Routes['GET /foo']
重命名为像type FooRoute = Routes['Get /foo']
这样的类型别名,这样您现在只需要FooRoute['query']
。
// contrived example from original post, modified
import { applyDecorators, RequestMapping, RequestMethod, Controller, Query } from '@nestjs/common'
type Routes = {
'GET /foo': {
query: { name: string }
response: { foo: true }
}
'GET /bar': {
query: {},
response: { bar: true }
}
}
export function ApplyRoute(routeName: keyof Routes): MethodDecorator {
const [method, path] = routeName.split(' ') as ['GET' | 'POST', string]
return applyDecorators(
RequestMapping({ path, method: RequestMethod[method] }),
)
}
type FooRoute = Routes['GET /foo']
@Controller()
export class MyController {
@ApplyRoute('GET /foo')
async findAll(
@Query() { name }: FooRoute['query'], // repeated route name
): Promise<FooRoute['response']> { // repeated route name
return { foo: true }
}
}
所以现在有了更少的重复的代码,但最后它仍然是重复的。