
大綱
本章主要講解一些ts的高級用法,涉及以下內(nèi)容:
- 類型斷言與類型守衛(wèi)
- in關(guān)鍵詞和is關(guān)鍵詞
- 類型結(jié)構(gòu)
- 裝飾器 ?
- Reflect Metadata 元編程
這篇稍微偏難一點,本文講解(不會講)的地方不是很多,主要以實例代碼的形式展示,重點在歸納和整理(搬筆記),建議不懂的地方查閱文檔或者是搜索 QAQ
往期推薦:
類型斷言與類型守衛(wèi)
簡單而言,做的就是確保類型更加的安全
- 單斷言
interface Student {
name?: string,
age?: number,
}
- 雙重斷言
const sudent1 = '男' as string as Student
- 類型守衛(wèi)
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關(guān)鍵詞和is關(guān)鍵詞
- in 關(guān)鍵詞 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 關(guān)鍵詞, 把參數(shù)的范圍縮小化
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')
類型結(jié)構(gòu)
- 字面量類型
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
- 聯(lián)合類型
const name: string | number = '1111' // 只能是字符串或者數(shù)字
// 聯(lián)合類型辨識
// 比如場景: 新增(無需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定義
它和接口的用法很像但又有本質(zhì)的區(qū)別:
- interface 有extends 和 implements(類實現(xiàn)接口的方法)
- 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 本質(zhì)就是一個函數(shù), 作用在于可以讓其它函數(shù)不在改變代碼的情況下,增加額外的功能,適合面向切面的場景,比如我去要在某個地方附加日志的功能,它最終返回的是一個函數(shù)對象。一些框架中其實也用到了裝飾器,比如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
- 方法裝飾器
// 方法裝飾器
// 聲明裝飾器函數(shù)
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}
// 打印結(jié)果,檢查是否成功修改實例方法
console.log(xmz.say()); // 發(fā)現(xiàn)報錯了 TypeError: Cannot assign to read only property 'say' of object '#<Person>'
- 參數(shù)裝飾器:
參數(shù)裝飾器可以提供信息,給比如給類原型添加了一個新的屬性,屬性中包含一系列信息,這些信息就被成為「元數(shù)據(jù)」,然后我們就可以使用另外一個裝飾器來讀取「元數(shù)據(jù)」。
- target —— 當前對象的原型,也就是說,假設(shè) Person1 是當前對象,那么當前對象 target 的原型就是 Person1.prototype
- propertyKey —— 參數(shù)的名稱,上例中指的就是 get
- index —— 參數(shù)數(shù)組中的位置,比如上例中參數(shù) 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)
- 裝飾器工廠 往往我們不推薦一個類身上綁定過多的裝飾器,而是希望統(tǒng)一化去處理
// 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}`
}
}
// 聲明裝飾器構(gòu)造函數(shù)
// 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
}
// 參數(shù)裝飾器
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("沒找到裝飾器函數(shù)")
}
}
// 然后用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}`
}
}
// 操作順序:
// 由上至下依次對裝飾器表達式求值。
// 求值的結(jié)果會被當作函數(shù),由下至上依次調(diào)用。
Reflect Metadata 元編程
<span style='color:#ff502c;background: #fff5f5;'> Reflect Metadata </span>屬于 ES7 的一個提案,它的主要作用就是在聲明的時候添加和讀取元數(shù)據(jù)。目前需要引入 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'
基本參數(shù):
- Metadata Key: 元數(shù)據(jù)的Key,本質(zhì)上內(nèi)部實現(xiàn)是一個Map對象,以鍵值對的形式儲存元數(shù)據(jù)
- Metadata Value: 元數(shù)據(jù)的Value,這個容易理解
- Target: 一個對象,表示元數(shù)據(jù)被添加在的對象上
- Property: 對象的屬性,元數(shù)據(jù)不僅僅可以被添加在對象上,也可以作用于屬性,這跟裝飾器類似 --- 所作用的屬性
@Reflect.metadata('class', 'Person5')
class Person5 {
@Reflect.metadata('method', 'say')
say(): string {
return 'say'
}
}
// 獲取元數(shù)據(jù)
Reflect.getMetadata('class', Person5) // 'Person5'
Reflect.getMetadata('method', new Person5, 'say') // 'say'
// 這里為啥要new Person5 ?
// 原因就在于元數(shù)據(jù)是被添加在了實例方法上,因此必須實例化才能取出,要想不實例化,
// 則必須加在靜態(tài)方法上.
- 內(nèi)置元數(shù)據(jù)(不是自己添加的自帶的)
// 獲取方法的類型 --- design:type 作為 key 可以獲取目標的類型
const type = Reflect.getMetadata("design:type", new Person5, 'say') // [Function: Function]
// 獲取參數(shù)的類型,返回數(shù)組 --- design:paramtypes 作為 key 可以獲取目標參數(shù)的類型
const typeParam = Reflect.getMetadata("design:paramtypes", new Person5, 'say') // [Function: String]
// 元數(shù)據(jù)鍵獲取有關(guān)方法返回類型的信息 ----使用 design:returntype :
const typeReturn = Reflect.getMetadata("design:returntype", new Person, 'say')
// [Function: String]
實踐
實現(xiàn)以下需求: 后臺路由管理, 實現(xiàn)一個控制器Controller 來管理路由中的方法, 暫時不考慮接收請求參數(shù)
@Controller('/list')
class List {
@Get('/read')
readList() {
return 'hello world';
}
@Post('/edit')
editList() {}
}
1, 需求肯定是需要實現(xiàn)一個Controller裝飾器工廠
const METHOD_METADATA = 'method'
const PATH_METADATA = 'path'
// 裝飾器工廠函數(shù),接收path返回對應(yīng)的裝飾器
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target) // 為裝飾器添加元數(shù)據(jù)
}
}
2, 接著需要實現(xiàn) Get Post 等方法裝飾器: --- 接收方法參數(shù)并返回對應(yīng)路徑的裝飾器函數(shù).實際上是一個柯里化函數(shù) ,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù).
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')
到這里為止我們已經(jīng)可以向Class中添加各種必要的元數(shù)據(jù)了,但是我們還差一步,就是讀取元數(shù)據(jù)。
// 判斷是否為構(gòu)造函數(shù)
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]';
}
我們需要一個函數(shù)來讀取整個Class中的元數(shù)據(jù):
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'
// }
// ]
如果對大家有幫助記得點贊個~ , 如有錯誤請指正, 我們一起解決,一起進步。