大綱
本章主要講解一些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'
// }
// ]
複製程式碼
如果對大家有幫助記得點贊個~ , 如有錯誤請指正, 我們一起解決,一起進步。