大纲
本章主要讲解一些ts的高级用法,涉及以下内容:
- 类型断言与类型守卫
- in关键词和is关键词
- 类型结构
- 装饰器 ❤
- Reflect Metadata 元编程
这篇稍微偏难一点,本文讲解(不会讲)的地方不是很多,主要以实例代码的形式展示,重点在归纳和整理(搬笔记),建议不懂的地方查阅文档或者是搜索 QAQ
类型断言与类型守卫
简单而言,做的就是确保类型更加的安全
- 单断言
interface Student { name?: string, age?: number, } 复制代码
- 双重断言
const sudent1 = '男' as string as Student 复制代码
- 类型守卫
class Test1 { name = 'lili' age = 20 } class Test2 { sex = '男' } function test(arg: Test1 | Test2) { if(arg instanceof Test1) { console.log(arg.age, arg.name) } if(arg instanceof Test2) { console.log(arg.sex) } } 复制代码
in关键词和is关键词
- in 关键词 x属性存在于y中
function test1(arg: Test1 | Test2) { if('name' in arg) { console.log(arg.age, arg.name) } if('sex' in arg) { console.log(arg.sex) } } 复制代码
- is 关键词, 把参数的范围缩小化
function user10(name: any): name is string { // is 是正常的没报错 return name === 'lili' } function user11(name: any): boolean { return name === 'lili' } function getUserName(name: string | number) { if(user10(name)) { console.log(name) console.log(name.length) // 换成boolean就会报错 user11(name) // Property 'length' does not exist on type 'string | number'. // Property 'length' does not exist on type 'number'.ts( } } getUserName('lili') 复制代码
类型结构
- 字面量类型
type Test = { op: 'test', // 字面量类型 name: string, } function test2(arg: Test) { if(arg.op === 'test') { console.log(arg.name) } } 复制代码
- 交叉类型,在ts中使用混入模式(传入不同对象,返回拥有所有对象属性)需要使用交叉类型
function test3<T extends object, U>(obj1: T, obj2: U): T & U { const result = <T & U>{}; // 交叉类型 for(let name in obj1) { (<T>result)[name] = obj1[name] } for(let name in obj2) { if(!result.hasOwnProperty(name)) { (<U>result)[name] = obj2[name] } } return result } const o = test3({name: 'lili'}, {age: 20}) // o.name o.age --- ok 复制代码
- 联合类型
const name: string | number = '1111' // 只能是字符串或者数字 // 联合类型辨识 // 比如场景: 新增(无需id) 和 查询(需要id) type List = | { action: 'add', form: { name: string, age: number, } } | { action: 'select', id: number, } const getInfo = (arg: List) => { if(arg.action === 'add') { // .... ad }else if(arg.action === 'select') { // .... select } } getInfo({action: 'select', id: 0}) 复制代码
- 类型别名 type定义 它和接口的用法很像但又有本质的区别:
- interface 有extends 和 implements(类实现接口的方法)
- interface 接口合并声明
type age = number const p: age = 20 // 泛型中的运用 type Age<T> = { age: T } const ageObj: Age<number> = { age: 20} 复制代码
- 属性自引
type Age1<T> = { name: number prop: Age1<T> // 引用自己的属性 } 复制代码
装饰器
装饰器这里要提一下, 最初装饰器是在python中使用的,在java中叫注解,后来js中也慢慢运用起来了,不过要借助打包工具。说一下这个装饰器是干嘛?从字面意思上理解,装饰,就是为其赋予。比如装房子,或者打扮自己。
Decorator 本质就是一个函数, 作用在于可以让其它函数不在改变代码的情况下,增加额外的功能,适合面向切面的场景,比如我去要在某个地方附加日志的功能,它最终返回的是一个函数对象。一些框架中其实也用到了装饰器,比如nest.js框架 angular框架 还有react的一些库等等, 如果你看到 @func 这样的代码,无疑就是它了。
为什么要这里提一下ts中的装饰器呢,因为它会赋予更加安全的类型,使得功能更完备,另外可以在ts中直接被编译。
- 类装饰器
function addAge(constructor: Function) { constructor.prototype.age = 18; } @addAge class Person_{ name: string; age: number; constructor() { this.name = 'xiaomuzhu'; this.age = 20 } } let person_ = new Person_(); console.log(person_.age); // 18 复制代码
- 方法装饰器
// 方法装饰器 // 声明装饰器函数 function decorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) { console.log(target); console.log("prop " + propertyKey); console.log("desc " + JSON.stringify(descriptor) + "\n\n"); descriptor.writable = false; // 禁用方法的: 可写性 意味着只能只读 } class Person{ name: string; constructor() { this.name = 'lili'; } @decorator say(){ return 'say'; } @decorator static run(){ return 'run'; } } const xmz = new Person(); // 修改实例方法say xmz.say = function() { return 'say' } // Person { say: [Function] } // prop say // desc {"writable":true,"enumerable":true,"configurable":true} // [Function: Person] { run: [Function] } // prop run // desc {"writable":true,"enumerable":true,"configurable":true} // 打印结果,检查是否成功修改实例方法 console.log(xmz.say()); // 发现报错了 TypeError: Cannot assign to read only property 'say' of object '#<Person>' 复制代码
- 参数装饰器: 参数装饰器可以提供信息,给比如给类原型添加了一个新的属性,属性中包含一系列信息,这些信息就被成为「元数据」,然后我们就可以使用另外一个装饰器来读取「元数据」。
- target —— 当前对象的原型,也就是说,假设 Person1 是当前对象,那么当前对象 target 的原型就是 Person1.prototype
- propertyKey —— 参数的名称,上例中指的就是 get
- index —— 参数数组中的位置,比如上例中参数 name 的位置是 1, message 的位置为 0
function decotarots(target: object, propertyKey: string, index: number) { console.log(target, propertyKey, index) } class Person1 { get(@decotarots name: string, @decotarots age: number): string { return `name: ${name} age: ${age}` } } const person = new Person1() person.get('lili', 20) 复制代码
- 装饰器工厂 往往我们不推荐一个类身上绑定过多的装饰器,而是希望统一化去处理
// 1. 本来的代码 @DecoratorClass class Person2 { @DecoratorProp public name: string @DecoratorProp public age: number constructor(name: string, age: number) { this.name = name this.age = age } @DecoratorMethod public get(@DecoratorArguments name: string, @DecoratorArguments age: number): string { return `name: ${name} age: ${age}` } } // 声明装饰器构造函数 // class 装饰器 function DecoratorClass(target: typeof Person2) { console.log(target) // [Function: Person2] } // 属性装饰器 function DecoratorProp(target: any, propertyKey: string) { console.log(propertyKey) // name age } // 方法装饰器 function DecoratorMethod(target: any, propertyKey: string) { console.log(propertyKey) // get } // 参数装饰器 function DecoratorArguments(target: object, propertyKey: string, index: number) { console.log(index) // 0 } 复制代码
// 2. 改造后的代码 function log(...args: any) { switch(args.length) { case 1: return DecoratorClass.apply(this, args) case 2: return DecoratorMethod.apply(this, args) case 3: if(typeof args[2] === "number") { return DecoratorArguments.apply(this, args) } return DecoratorMethod.apply(this, args) //也有可能是 descriptor: PropertyDescriptor 属性 default: throw new Error("没找到装饰器函数") } } // 然后用log代替即可 @log class Person3 { @log public name: string @log public age: number constructor(name: string, age: number) { this.name = name this.age = age } @log public get(@log name: string, @log age: number): string { return `name: ${name} age: ${age}` } } 复制代码
- 同一声明-多个装饰器
class Person4 { // 声明多个装饰器 @log @DecoratorMethod public get(@log name: string, @log age: number): string { return `name: ${name} age: ${age}` } } // 操作顺序: // 由上至下依次对装饰器表达式求值。 // 求值的结果会被当作函数,由下至上依次调用。 复制代码
Reflect Metadata 元编程
Reflect Metadata 属于 ES7 的一个提案,它的主要作用就是在声明的时候添加和读取元数据。目前需要引入 npm 包才能使用,另外需要在 tsconfig.json 中配置 emitDecoratorMetadata.
npm i reflect-metadata --save 复制代码
QAQ 这就变得和java中的注解很像很像了.... 作用: 可以通过装饰器来给类添加一些自定义的信息,然后通过反射将这些信息提取出来,也可以通过反射来添加这些信息
@Reflect.metadata('name', 'A') class A { @Reflect.metadata('hello', 'world') public hello(): string { return 'hello world' } } Reflect.getMetadata('name', A) // 'A' Reflect.getMetadata('hello', new A()) // 'world' 复制代码
基本参数:
- Metadata Key: 元数据的Key,本质上内部实现是一个Map对象,以键值对的形式储存元数据
- Metadata Value: 元数据的Value,这个容易理解
- Target: 一个对象,表示元数据被添加在的对象上
- Property: 对象的属性,元数据不仅仅可以被添加在对象上,也可以作用于属性,这跟装饰器类似 --- 所作用的属性
@Reflect.metadata('class', 'Person5') class Person5 { @Reflect.metadata('method', 'say') say(): string { return 'say' } } // 获取元数据 Reflect.getMetadata('class', Person5) // 'Person5' Reflect.getMetadata('method', new Person5, 'say') // 'say' // 这里为啥要new Person5 ? // 原因就在于元数据是被添加在了实例方法上,因此必须实例化才能取出,要想不实例化, // 则必须加在静态方法上. 复制代码
- 内置元数据(不是自己添加的自带的)
// 获取方法的类型 --- design:type 作为 key 可以获取目标的类型 const type = Reflect.getMetadata("design:type", new Person5, 'say') // [Function: Function] // 获取参数的类型,返回数组 --- design:paramtypes 作为 key 可以获取目标参数的类型 const typeParam = Reflect.getMetadata("design:paramtypes", new Person5, 'say') // [Function: String] // 元数据键获取有关方法返回类型的信息 ----使用 design:returntype : const typeReturn = Reflect.getMetadata("design:returntype", new Person, 'say') // [Function: String] 复制代码
实践
实现以下需求: 后台路由管理, 实现一个控制器 Controller 来管理路由中的方法, 暂时不考虑接收请求参数
@Controller('/list') class List { @Get('/read') readList() { return 'hello world'; } @Post('/edit') editList() {} } 复制代码
1, 需求肯定是需要实现一个Controller装饰器工厂
const METHOD_METADATA = 'method' const PATH_METADATA = 'path' // 装饰器工厂函数,接收path返回对应的装饰器 const Controller = (path: string): ClassDecorator => { return target => { Reflect.defineMetadata(PATH_METADATA, path, target) // 为装饰器添加元数据 } } 复制代码
2, 接着需要实现 Get Post 等方法装饰器: --- 接收方法参数并返回对应路径的装饰器函数.实际上是一个柯里化函数 ,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数.
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => { return (target, key, descriptor) => { Reflect.defineMetadata(PATH_METADATA, path, descriptor.value!) Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value!) } } const GET = createMappingDecorator('GET') const POST = createMappingDecorator('POST') 复制代码
到这里为止我们已经可以向Class中添加各种必要的元数据了,但是我们还差一步,就是读取元数据。
// 判断是否为构造函数 function isConstructor(f: any): boolean { try { new f(); } catch (err) { // verify err is the expected error and then return false; } return true; } function isFunction(functionToCheck: any): boolean { return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'; } 复制代码
我们需要一个函数来读取整个Class中的元数据:
function mapRoute(instance: Object) { const prototype = Object.getPrototypeOf(instance) // 筛选出类的 methodName const methodsNames = Object.getOwnPropertyNames(prototype) .filter(item => !isConstructor(item) && isFunction(prototype[item])); return methodsNames.map(methodName => { const fn = prototype[methodName]; // 取出定义的 metadata const route = Reflect.getMetadata(PATH_METADATA, fn); const method = Reflect.getMetadata(METHOD_METADATA, fn); return { route, method, fn, methodName } }) } 复制代码
使用:
@Controller('/list') class Articles { @GET('/read') readList() { return 'hello world'; } @POST('/edit') editList() {} } Reflect.getMetadata(PATH_METADATA, Articles) const res = mapRoute(new Articles()) console.log(res); // [ // { // route: '/list', // method: undefined, // fn: [Function: Articles], // methodName: 'constructor' // }, // { // route: '/read', // method: 'GET', // fn: [Function], // methodName: 'readList' // }, // { // route: '/edit', // method: 'POST', // fn: [Function], // methodName: 'editList' // } // ]