typescript修炼指南(三)

大纲 本章主要讲解一些ts的高级用法,涉及以下内容: 类型断言与类型守卫 in关键词和is关键词 类型结构 装饰器 ❤ Reflect Metadata 元编程 这篇稍微偏难一点,本文讲解(不会讲)的地方不是很多,主要以实例代码的形式展示,重点在归纳和整理(搬笔记),建议不懂的地方查阅文档或者是搜索 QAQ 往期推荐: typescript修炼指南(一) typescript修炼指南(二) 类型断言与类型守卫 简单而言,做的就是确保类型更加的安全 单断言 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(obj1: T, obj2: U): T & U { const result = {}; // 交叉类型 for(let name in obj1) { (result)[name] = obj1[name] } for(let name in obj2) { if(!result.hasOwnProperty(name)) { (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 = { age: T } const ageObj: Age = { age: 20} 属性自引 type Age1 = { name: number prop: Age1 // 引用自己的属性 } 装饰器 装饰器这里要提一下, 最初装饰器是在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 '#' 参数装饰器: 参数装饰器可以提供信息,给比如给类原型添加了一个新的属性,属性中包含一系列信息,这些信息就被成为「元数据」,然后我们就可以使用另外一个装饰器来读取「元数据」。 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' // } // ] 如果对大家有帮助记得点赞个~ , 如有错误请指正, 我们一起解决,一起进步。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):