Express-router中的get方法是如何使用ts约束的?
hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~
起源
在上一篇文章中,最后实现了一个小案例,使用装饰器实现get的接口请求,
下面是这个小Demo的简略版:
import { Router } from 'express'
export const router: Router = Router();
type MethodType = "get" | "post"
// 封装方法装饰器
function reqMethodDecorator(methodType: MethodType) {
return function (path: string): MethodDecorator {
return (targetPrototype, methodname) => {
Reflect.defineMetadata('path', path, targetPrototype, methodname)
Reflect.defineMetadata('methodtype', methodType, targetPrototype, methodname)
}
}
}
export const Get = reqMethodDecorator("get")
@Controller("/")
class FoodController {
@Get("/showFood/:foodname/:price")
showFood(req: Request, res: Response): void {
res.setHeader("Content-Type", "text/html; charset=utf-8")
let foodname = req.params.foodname
let price = req.params.price
res.write(`food:${foodname}`);
res.write(`food:${price}`);
res.write("very good");
res.write("nice")
res.end();
}
}
export function Controller(rootPath: string): ClassDecorator {
return (target) => {
for(let methodName in target.prototype) {
let routerPath = Reflect.getMetadata('path', target.prototype, methodName)
let reqName: MethodType = Reflect.getMetadata('methodtype', target.prototype, methodName)
const targetMethodfunc: RequestHandler = target.prototype[methodName];
if(routerPath && reqName) {
router[reqName](routerPath, targetMethodfunc)
}
}
}
}
简单来说就是使用实例方法装饰器注册请求路径和请求方式,然后使用类装饰器获取所有的方法,通过定义在实例方法上的元数据来对信息进行组合和提取,完成get
请求的实现逻辑。
在get
请求函数中,支持在url
中使用 : 来传递参数,然后将获取到的foodname
和 price
这两个参数作为返回值写入到页面中。
@Controller("/")
class FoodController {
@Get("/showFood/:foodname/:price")
showFood(req: Request, res: Response): void {
res.setHeader("Content-Type", "text/html; charset=utf-8")
let foodname = req.params.foodname
let price = req.params.price
res.write(`food:${foodname}`);
res.write(`food:${price}`);
res.write("very good");
res.write("nice")
res.end();
}
}
在get
的处理函数中,我们通过req
这个参数获取到了传入的foodname
和price
这两个属性,然后写入页面返回。
@Get("/showFood/:foodname/:price")
// 也就相等于
router.get('/showFood/:foodname/:price')
当我们把鼠标放到get
方法上的时候,神奇的事情发生了,ts已经对转化后的结果进行了约束!
查找源码
那么express
是如何实现对字符串进行自动提取的呢,首先找到router
这个方法:
import { Router } from 'express'
export const router: Router = Router();
router
是express
中提供的Router
方法的返回结果,使用Router
约束,那么就来看看Router
这个类型是怎样定义的:
interface Router extends core.Router {}
可以看到在express
内部Router
继承了core.Router
,由于core
是有一整个文件导出,所以继续在core
文件下寻找Router
类型:
import * as core from 'express-serve-static-core';
Router
继承自IRouter
:
export interface Router extends IRouter {}
在IRouter
接口中定义了所有的方法:
export interface IRouter extends RequestHandler {
param(name: string, handler: RequestParamHandler): this;
/**
* Alternatively, you can pass only a callback, in which case you have the opportunity to alter the app.param()
*
* @deprecated since version 4.11
*/
param(callback: (name: string, matcher: RegExp) => RequestParamHandler): this;
/**
* Special-cased "all" method, applying the given route `path`,
* middleware, and callback to _every_ HTTP method.
*/
all: IRouterMatcher<this, 'all'>;
get: IRouterMatcher<this, 'get'>;
post: IRouterMatcher<this, 'post'>;
put: IRouterMatcher<this, 'put'>;
delete: IRouterMatcher<this, 'delete'>;
patch: IRouterMatcher<this, 'patch'>;
options: IRouterMatcher<this, 'options'>;
head: IRouterMatcher<this, 'head'>;
// 省略...
}
这里我们只寻找get
相关,进入到 IRouterMatcher
接口:
export interface IRouterMatcher<
T,
Method extends 'all' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' = any
> {
<
Route extends string,
P = RouteParameters<Route>,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
LocalsObj extends Record<string, any> = Record<string, any>
>(
// (it's used as the default type parameter for P)
// eslint-disable-next-line no-unnecessary-generics
path: Route,
// (This generic is meant to be passed explicitly.)
// eslint-disable-next-line no-unnecessary-generics
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, LocalsObj>>
): T;
// 省略...
(path: PathParams, subApplication: Application): T;
}
IRouterMatcher
接口是一个重载函数,这里依然只关注与get
相关,最后可以简化为:
<
Route extends string,
P = RouteParameters<Route>,
>(
path: Route,
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, LocalsObj>>
): any;
泛型Route
就是我们传入的url
地址,这里为参数path
的约束,而泛型参数P
是使用RouteParameters
类型传入url
处理约束。
在RouteParameters
类型中对url
处理:
export interface ParamsDictionary {
[key: string]: string;
}
type RemoveTail<S extends string, Tail extends string> = S extends `${infer P}${Tail}` ? P : S;
type GetRouteParameter<S extends string> = RemoveTail<
RemoveTail<RemoveTail<S, `/${string}`>, `-${string}`>,
`.${string}`
>;
// prettier-ignore
export type RouteParameters<Route extends string> = string extends Route
? ParamsDictionary
: Route extends `${string}(${string}`
? ParamsDictionary //TODO: handling for regex parameters
: Route extends `${string}:${infer Rest}`
? (
GetRouteParameter<Rest> extends never
? ParamsDictionary
: GetRouteParameter<Rest> extends `${infer ParamName}?`
? { [P in ParamName]?: string }
: { [P in GetRouteParameter<Rest>]: string }
) &
(Rest extends `${GetRouteParameter<Rest>}${infer Next}`
? RouteParameters<Next> : unknown)
: {};
这就是传说中的类型体操了😂,接下来就一块拆解并学习它。
拆解
首先来看一下RouteParameters
类型的定义:
type RouteParameters<Route extends string>
extends
extends
在泛型参数中通常用于判断是否属于某个类型,例如:
type test1 = 1 extends number ? 'y' : 'n'
type test2 = '2' extends number ? 'y' : 'n'
type test3 = '3' extends string ? 'y' : 'n'
而在typescript
中,还存在一种分布式条件判断,如果判断表达式左侧是使用泛型传入,而且参数类型是一个联合类型,就会出发联合类型:
type test44 = 1 | 3
type test4<T> = T extends 1 | 2 | 4 | 5 ? 'y' : 'n'
type test5 = test4<test44>
试想一下test5
会是什么类型?可能大部分人都会说是n
,那就来看一下:
test5
的类型被推断为"y" | "n"
,这是为什么呢?在typescript
中,对联合类型的判断是分别进行的,比如上面实际执行是这样的:
type t1 = 1 extends 1 | 2 | 4 | 5 ? 'y' : 'n'
type t2 = 3 extends 1 | 2 | 4 | 5 ? 'y' : 'n'
type test5 = t1 | t2 // "y" | "n"
如果不想使用这种特性,可以使用[]包裹:
type test44 = 1 | 3
type test4<T> = [T] extends [1 | 2 | 4 | 5] ? 'y' : 'n'
type test5 = test4<test44>
在 RouteParameters
泛型参数中 extends
将Route
泛型参数约束为string
。
接下来看第一个判断,后续逻辑以xxx代替:
string extends Route
? ParamsDictionary
:
// xxx
在泛型参数定义的时候已经被约束为Route extends string
了,为啥这里还要再判断string extends Route
?这里是为了判断是否是直接传入了一个string
类型,因为只有传入url
的字符串字面量类型我们才会进行处理。
ParamsDictionary
其实是定义了索引签名类型,一个key
数量不定,类型为string
,值为string
的对象:
export interface ParamsDictionary {
[key: string]: string;
}
在 typescript
中使用 [key: any]
来定义不确定数量的 key
,在此对象上可以任意定义值:
let obj: ParamsDictionary = {
name: 'gua',
addr: 'abc'
}
obj.other = 'aa'
然后看一下下一个判断:
Route extends `${string}(${string}`
? ParamsDictionary //TODO: handling for regex parameters
:
// xxx
这里主要判断 url
字符串中是否包含(
,如果存在(
,依然返回ParamsDictionary
。
下面就是到了核心的处理逻辑:
Route extends `${string}:${infer Rest}`
? (
GetRouteParameter<Rest> extends never
? ParamsDictionary
: GetRouteParameter<Rest> extends `${infer ParamName}?`
? { [P in ParamName]?: string }
: { [P in GetRouteParameter<Rest>]: string }
) &
(Rest extends `${GetRouteParameter<Rest>}${infer Next}`
? RouteParameters<Next> : unknown)
: {};
在进入到主逻辑之前先来了解一下其中的几个辅助类型:GetRouteParameter
,RemoveTail
因为RemoveTail
类型是最底层的调用,所以先来看一下这个类型:
type RemoveTail<S extends string, Tail extends string> = S extends `${infer P}${Tail}` ? P : S;
RemoveTail
接收两个泛型参数:S 和 Tail
,全都约束为string
类型,然后使用条件类型判断S是否是一个模版类型的子类型。
模版类型
那么条件类型是怎样判断呢,咱们先从模版类型入手,extends
的右侧是一个模版字符串的语法:
S extends `${infer P}${Tail}`
在typescript
中可以使用``模版语法符号进行类型匹配,比如:
type str1 = `hello ${string}`
const c1: str1 = 'hello mi'
这句话的意思是约束一个空格前为hello
的字符串字面量类型和空格后的任意string
类型,如果我们定义一个别的字符串,那么会提示类型错误:
const c2: str1 = 'hihaha'
或者还可以更灵活一点,使用泛型来定义:
type str2<T extends string> = `hello ${T}`
const c3: str2<'gua'> = 'hello gua'
我们可以更佳精确的控制字符串字面量类型。
infer
而在这个类型约束中还用到了关键字infer
,这也是在typescript
中非常重要的内容。infer
的功能是“提取”,可以提取字符串,数组,函数的置顶内容:
- 字符串
获取字符串指定位置的字符串
type test2 = 'hello-gua' extends `${infer S}-${string}` ? S : never
- 数组
获取数组第一项
type test1 = [1, 2, 3, 4] extends [infer R, ...unknown[]] ? R : never
获取最后一项
type test1 = [1, 2, 3, 4] extends [...unknown[], infer R] ? R : never
- 函数
获取参数类型
type test5 = ((name: string) => any) extends ((name: infer A) => any) ? A : never
获取函数返回值类型
type test6 = ((name: string) => boolean) extends ((...args: any[]) => infer R) ? R : never
在看完模版类型和infer
之后就可以来正式看一下我们的RemoveTail
类型了,其实在了解完前面的知识之后再看RemoveTail
就很好理解了:
type RemoveTail<S extends string, Tail extends string> = S extends `${infer P}${Tail}` ? P : S;
RemoveTail
类型是实现的功能就是删除指定的字符串其中泛型Tail
是要被删掉的字符串,使用infer
提取剩下的字符串:
type test66 = RemoveTail<'hello-gua', '-gua'>
那么调用他的GetRouteParameter
类型就更好理解了:
type GetRouteParameter<S extends string> = RemoveTail<
RemoveTail<RemoveTail<S, `/${string}`>, `-${string}`>,
`.${string}`
>;
使用RemoveTail
依次匹配删除一段字符串中的 /(后面的字符串),-(后面的字符串),.(后面的字符串)。
因为我们并不知道一段字符串中三个符号在什么位置,所以调用三次RemoveTail
删除三个符号后面的字符串内容。
假如有字符串hello-gua/abc.cde
:
type test77 = GetRouteParameter<'hello-gua/abc.cde'>
然后回到正题:
Route extends `${string}:${infer Rest}`
? (
GetRouteParameter<Rest> extends never
? ParamsDictionary
: GetRouteParameter<Rest> extends `${infer ParamName}?`
// 处理可选属性
? { [P in ParamName]?: string }
: { [P in GetRouteParameter<Rest>]: string }
) &
(Rest extends `${GetRouteParameter<Rest>}${infer Next}`
? RouteParameters<Next> : unknown)
: {};
首先url
字符串提取 :后面的所有字符串,如果我们有字符串 /showFood/:foodname/:price
,那么本次处理之后,剩下的Rest
为 foodname/:price
。
接下来判断取出的值是否为never
?显然这里不是,于是接着往下处理,调用GetRouteParameter
,GetRouteParameter
会依次删除 /``.``-
后面的字符串,于是最后只剩下foodname
,最后取出foodname
,判断是否包含?
的url
,因为在url
中支持配置可选属性:
router.get('/showFood/:foodname?/:price')
// 最后会提取为
{
foodname?: string
} & {
price: string
}
最后使用 in
来映射为对象结构。
in
in
为映射类型,右侧一般会跟一个联合类型,使用in
操作符可以对该联合类型进行迭代。 其作用类似js
中的for...in
或者for...of
type Animals = 'pig' | 'cat' | 'dog'
type animals = {
[key in Animals]: string
}
// type animals = {
// pig: string; //第一次迭代
// cat: string; //第二次迭代
// dog: string; //第三次迭代
// }
最后终于处理完了&
左侧的内容,也就是第一个属性,接下来继续递归调用RouteParameters
向后处理,那么如何判断已经处理完的内容呢?依旧是模板类型做提取:
(Rest extends `${GetRouteParameter<Rest>}${infer Next}`)
相当于:
(Rest extends `foodname${infer Next}`)
提取到的Next
为/:price
,然后继续递归处理。整体过程如下:
快乐的玩耍
那么如何应用到我们的日常开发中呢,比如现在我们也有一个实现解析字符串的功能:
type StrType = <
Route extends string
>(
path: Route,
) => RouteParameters<Route>;
const testfn: StrType = function (path) {
const strAry = path.split('/')
return strAry.reduce((cur, next)=>{
if(next.includes(":")) {
const key = next.slice(1)
// 简单处理value为a
cur[key] = 'a'
}
return cur
}, {} as any)
}
const str = '/showPeople/:name/:age/:addr'
let p = testfn(str)
如果将这个类型应用到我们的方法中,那么我们在使用执行结果的返回值时,会自动获取typescript
的智能提示:
只能说学好typescript
类型体操,真香! 😂😂
写在最后 ⛳
未来可能会更新typescript
和react
基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳
转载自:https://juejin.cn/post/7214885024118046779