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'
// }
// ]
如果对大家有帮助记得点赞个~ , 如有错误请指正, 我们一起解决,一起进步。
发表评论 (审核通过后显示评论):