TypeScript技術知識整理

TypeScript技術知識整理

文章目錄


一、環境搭建與編譯執行

TypeScript 編寫的程序並不能直接通過瀏覽器運行,我們需要先通過 TypeScript 編譯器把 TypeScript 代碼編譯成 JavaScript 代碼

TypeScript 的編譯器是基於 Node.js 的,所以我們需要先安裝 Node.js

1.安裝 TypeScript 編譯器

通過 NPM 包管理工具安裝 TypeScript 編譯器

npm i -g typescript

安裝完成以後,我們可以通過命令 tsc 來調用編譯器

# 查看當前 tsc 編譯器版本
tsc -v

2.編寫代碼

代碼編輯器 - vscode

vsCodeTypeScript都是微軟的產品,vsCode 本身就是基於 TypeScript 進行開發的,vsCodeTypeScript 有着天然友好的支持

默認情況下,TypeScript 的文件的後綴爲 .ts

TypeScript 代碼

// ./src/helloWX.ts
let str: string = 'WST-WX';

3.編譯執行

使用我們安裝的 TypeScript 編譯器 tsc.ts 文件進行編譯

tsc ./src/helloKaiKeBa.ts

默認情況下會在當前文件所在目錄下生成同名的 js 文件

5.一些有用的編譯選項

編譯命令 tsc 還支持許多編譯選項,這裏我先來了解幾個比較常用的

–outDir

指定編譯文件輸出目錄

tsc --outDir ./dist ./src/helloWX.ts

–target

指定編譯的代碼版本目標,默認爲 ES3

tsc --outDir ./dist --target ES6 ./src/helloWX.ts

–watch

在監聽模式下運行,當文件發生改變的時候自動編譯

tsc --outDir ./dist --target ES6 --watch ./src/helloWX.ts

通過上面幾個例子,我們基本可以瞭解 tsc 的使用了,但是大家應該也發現了,如果每次編譯都輸入這麼一大堆的選項其實是很繁瑣的,好在TypeScript 編譯爲我們提供了一個更加強大且方便的方式,編譯配置文件:tsconfig.json,我們可以把上面的編譯選項保存到這個配置文件中

6.編譯配置文件

我們可以把編譯的一些選項保存在一個指定的 json 文件中,默認情況下 tsc 命令運行的時候會自動去加載運行命令所在的目錄下的 tsconfig.json 文件,配置文件格式如下

{
    // 推薦常用的配置格式
	"compilerOptions": {
		"outDir": "./dist", // 輸出路徑
		"target": "ES2015", // 定義編譯後的ES語法
        "watch": true,      // 是否開啓監聽模式
        "removeComments": true, // 是否移除TS文件中的註釋代碼
        “noImplicitAny:true, // 是否允許any類型推斷
        “strictNullChecks”: true, // 嚴格控制空值類型檢查
        
	},
  // ** : 所有目錄(包括子目錄)
  // * : 所有文件,也可以指定類型 *.ts
  "include": ["./src/**/*"]
}

有了單獨的配置文件,我們就可以直接運行

tsc

7.指定加載的配置文件

使用 --project-p 指定配置文件目錄,會默認加載該目錄下的 tsconfig.json 文件

tsc -p ./configs

也可以指定某個具體的配置文件

tsc -p ./configs/ts.json

二、類型系統初識

1.什麼是類型

程序 = 數據結構 + 算法 = 各種格式的數據 + 處理數據的邏輯

數據是有格式(類型)的

  • 數字、布爾值、字符
  • 數組、集合

程序是可能有錯誤的

  • 計算錯誤(對非數字類型數據進行一些數學運算)
  • 調用一個不存在的方法

不同類型的數據有不同的操作方式或方法,如:字符串類型的數據就不應該直接參與數學運算

2.動態類型語言 & 靜態類型語言

動態類型語言

程序運行期間才做數據類型檢查的語言,如:JavaScript

靜態類型語言

程序編譯期間做數據類型檢查的語言,如:Java

靜態類型語言的優缺點

優點

  • 程序編譯階段(配合IDE、編輯器甚至可以在編碼階段)即可發現一些潛在錯誤,避免程序在生產環境運行了以後再出現錯誤
  • 編碼規範、有利於團隊開發協作、也更有利於大型項目開發、項目重構
  • 配合IDE、編輯器提供更強大的代碼智能提示/檢查
  • 代碼即文檔

缺點

  • 麻煩
  • 缺少靈活性

動態類型語言的優缺點

優點

  • 靜態類型語言的缺點

缺點

  • 靜態類型語言的優點

靜態類型語言的核心 : 類型系統

3.什麼是類型系統

類型系統包含兩個重要組成部分

  • 類型標註(定義、註解) - typing
  • 類型檢測(檢查) - type-checking

4.類型標註

類型標註就是在代碼中給數據(變量、函數(參數、返回值))添加類型說明,當一個變量或者函數(參數)等被標註以後就不能存儲或傳入與標註類型不符合的類型

有了標註,TypeScript 編譯器就能按照標註對這些數據進行類型合法檢測。

有了標註,各種編輯器、IDE等就能進行智能提示

5.類型檢測

顧名思義,就是對數據的類型進行檢測。注意這裏,重點是類型兩字。

類型系統檢測的是類型,不是具體值(雖然,某些時候也可以檢測值),比如某個參數的取值範圍(1-100之間),我們不能依靠類型系統來完成這個檢測,它應該是我們的業務層具體邏輯,類型系統檢測的是它的值類型是否爲數字!

6.類型標註的基本語法格式

TypeScript 中,類型標註的基本語法格式爲:

數據載體:類型

TypeScript 的類型標註,我們可以分爲

  • 基礎的簡單的類型標註
  • 高級的深入的類型標註

7.基礎的簡單的類型標註

  • 基礎類型
  • 空和未定義類型
  • 對象類型
  • 數組類型
  • 元組類型
  • 枚舉類型
  • 無值類型
  • Never類型
  • 任意類型
  • 未知類型(Version3.0 Added)

8.基礎類型

基礎類型包含:stringnumberboolean

標註語法

let title: string = 'ts';
let n: number = 100;
let isOk: boolean = true;

9.空和未定義類型

因爲在 NullUndefined 這兩種類型有且只有一個值,在標註一個變量爲 NullUndefined 類型,那就表示該變量不能修改了

let a: null;
// ok
a = null;
// error
a = 1;

默認情況下 nullundefined 是所有類型的子類型。 就是說你可以把 nullundefined 其它類型的變量

let a: number;
// ok
a = null;

如果一個變量聲明瞭,但是未賦值,那麼該變量的值爲 undefined,但是如果它同時也沒有標註類型的話,默認類型爲 anyany 類型後面有詳細說明

// 類型爲 `number`,值爲 `undefined`
let a: number;
// 類型爲 `any`,值爲 `undefined`

小技巧

因爲 nullundefined 都是其它類型的子類型,所以默認情況下會有一些隱藏的問題

let a:number;
a = null;
// ok(實際運行是有問題的)
a.toFixed(1);

小技巧:指定 strictNullChecks 配置爲 true,可以有效的檢測 null 或者 undefined,避免很多常見問題

let a:number;
a = null;
// error
a.toFixed(1);

也可以使我們程序編寫更加嚴謹

let ele = document.querySelector('div');
// 獲取元素的方法返回的類型可能會包含 null,所以最好是先進行必要的判斷,再進行操作
if (ele) {
		ele.style.display = 'none';
}

10.對象類型

內置對象類型

JavaScript 中,有許多的內置對象,比如:Object、Array、Date……,我們可以通過對象的 構造函數 或者 來進行標註

let a: object = {};
// 數組這裏標註格式有點不太一樣,後面會在數組標註中進行詳細講解
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();


自定義對象類型

另外一種情況,許多時候,我們可能需要自定義結構的對象。這個時候,我們可以:

  • 字面量標註
  • 接口
  • 定義 或者 構造函數

字面量標註

let a: {username: string; age: number} = {
  username: 'zMouse',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;


優點 : 方便、直接

缺點 : 不利於複用和維護

接口

// 這裏使用了 interface 關鍵字,在後面的接口章節中會詳細講解
interface Person {
  username: string;
  age: number;
};
let a: Person = {
  username: 'WXin',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;


優點 : 複用性高

缺點 : 接口只能作爲類型標註使用,不能作爲具體值,它只是一種抽象的結構定義,並不是實體,沒有具體功能實現

類與構造函數

// 類的具體使用,也會在後面的章節中講解
class Person {
	constructor(public username: string, public age: number) {
  }
}
// ok
a.username;
a.age;
// error
a.gender;


優點 : 功能相對強大,定義實體的同時也定義了對應的類型

缺點 : 複雜,比如只想約束某個函數接收的參數結構,沒有必要去定一個類,使用接口會更加簡單

interface AjaxOptions {
    url: string;
    method: string;
}

function ajax(options: AjaxOptions) {}

ajax({
    url: '',
    method: 'get'
});


擴展

包裝對象:

這裏說的包裝對象其實就是 JavaScript 中的 StringNumberBoolean,我們知道 string 類型 和 String 類型並不一樣,在 TypeScript 中也是一樣

let a: string;
a = '1';
// error String有的,string不一定有(對象有的,基礎類型不一定有)
a = new String('1');

let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';


11.數組類型

TypeScript 中數組存儲的類型必須一致,所以在標註數組類型的時候,同時要標註數組中存儲的數據類型

使用泛型標註

// <number> 表示數組中存儲的數據類型,泛型具體概念後續會講
let arr1: Array<number> = [];
// ok
arr1.push(100);
// error
arr1.push('haha');


簡單標註

let arr2: string[] = [];
// ok
arr2.push('haha');
// error
arr2.push(1);


12.元組類型

元組類似數組,但是存儲的元素類型不必相同,但是需要注意:

  • 初始化數據的個數以及對應位置標註類型必須一致
  • 越界數據必須是元組標註中的類型之一(標註越界數據可以不用對應順序 - 聯合類型
let data1: [string, number] = ['wangxin', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);


13.枚舉類型

枚舉的作用組織收集一組關聯數據的方式,通過枚舉我們可以給一組有關聯意義的數據賦予一些友好的名字

enum HTTP_CODE {
  OK = 200,
  NOT_FOUND = 404,
  METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;


注意事項:

  • key 不能是數字
  • value 可以是數字,稱爲 數字類型枚舉,也可以是字符串,稱爲 字符串類型枚舉,但不能是其它值,默認爲數字:0
  • 枚舉值可以省略,如果省略,則:
    • 第一個枚舉值默認爲:0
    • 非第一個枚舉值爲上一個數字枚舉值 + 1
  • 枚舉值爲只讀(常量),初始化後不可修改

字符串類型枚舉

枚舉類型的值,也可以是字符串類型

enum URLS  {
  USER_REGISETER = '/user/register',
  USER_LOGIN = '/user/login',
  // 如果前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值
  INDEX = 0
}


注意:如果前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值

小技巧:枚舉名稱可以是大寫,也可以是小寫,推薦使用全大寫(通常使用全大寫的命名方式來標註值爲常量)

14.無值類型

表示沒有任何數據的類型,通常用於標註無返回值函數的返回值類型,函數默認標註類型爲:void

function fn():void {
  	// 沒有 return 或者 return undefined
}


strictNullChecksfalse 的情況下,undefinednull 都可以賦值給 void ,但是當 strictNullCheckstrue 的情況下,只有 undefined 纔可以賦值給 void

15.Never類型

當一個函數永遠不可能執行 return 的時候,返回的就是 never ,與 void 不同,void 是執行了 return, 只是沒有值,never 是不會執行 return,比如拋出錯誤,導致函數終止執行

function fn(): never {
  	throw new Error('error');
}


16.任意類型

有的時候,我們並不確定這個值到底是什麼類型或者不需要對該值進行類型檢測,就可以標註爲 any 類型

let a: any;


  • 一個變量申明未賦值且未標註類型的情況下,默認爲 any 類型
  • 任何類型值都可以賦值給 any 類型
  • any 類型也可以賦值給任意類型
  • any 類型有任意屬性和方法

注意:標註爲 any 類型,也意味着放棄對該值的類型檢測,同時放棄 IDE 的智能提示

小技巧:當指定 noImplicitAny 配置爲 true,當函數參數出現隱含的 any 類型時報錯

17.未知類型

unknow,3.0 版本中新增,屬於安全版的 any,但是與 any 不同的是:

  • unknow 僅能賦值給 unknowany
  • unknow 沒有任何屬性和方法

18.函數類型

JavaScript 函數是非常重要的,在 TypeScript 也是如此。同樣的,函數也有自己的類型標註格式

  • 參數
  • 返回值
函數名稱( 參數1: 類型, 參數2: 類型... ): 返回值類型;


function add(x: number, y: number): number {
  	return x + y;
}


三、接口

1.接口定義

前面我們說到,TypeScript 的核心之一就是對值(數據)所具有的結構進行類型檢查,除了一些前面說到基本類型標註,針對對象類型的數據,除了前面提到的一些方式以外,我們還可以通過: Interface (接口),來進行標註。

接口:對複雜的對象類型進行標註的一種方式,或者給其它代碼定義一種契約(比如:類)

接口的基礎語法定義結構特別簡單

interface Point {
    x: number;
    y: number;
}

上面的代碼定義了一個類型,該類型包含兩個屬性,一個 number 類型的 x 和一個 number 類型的 y,接口中多個屬性之間可以使用 逗號 或者 分號 進行分隔

我們可以通過這個接口來給一個數據進行類型標註

let p1: Point = {
    x: 100,
    y: 100
};

注意:接口是一種 類型 ,不能作爲 使用

interface Point {
    x: number;
    y: number;
}

let p1 = Point;	//錯誤

當然,接口的定義規則遠遠不止這些

2.可選屬性

接口也可以定義可選的屬性,通過 ? 來進行標註

interface Point {
    x: number;
    y: number;
    color?: string;
}

其中的 color? 表示該屬性是可選的

3.只讀屬性

我們還可以通過 readonly 來標註屬性爲只讀

interface Point {
    readonly x: number;
    readonly y: number;
}

當我們標註了一個屬性爲只讀,那麼該屬性除了初始化以外,是不能被再次賦值的

4.任意屬性

有的時候,我們希望給一個接口添加任意屬性,可以通過索引類型來實現

數字類型索引

interface Point {
    x: number;
    y: number;
    [prop: number]: number;
}

字符串類型索引

interface Point {
    x: number;
    y: number;
    [prop: string]: number;
}

數字索引是字符串索引的子類型

注意:索引簽名參數類型必須爲 stringnumber 之一,但兩者可同時出現

interface Point {
    [prop1: string]: string;
    [prop2: number]: string;
}

注意:當同時存在數字類型索引和字符串類型索引的時候,數字類型的值類型必須是字符串類型的值類型或子類型

interface Point1 {
    [prop1: string]: string;
    [prop2: number]: number;	// 錯誤
}
interface Point2 {
    [prop1: string]: Object;
    [prop2: number]: Date;	// 正確
}

5.使用接口描述函數

我們還可以使用接口來描述一個函數

interface IFunc {
  (a: string): string;
}

let fn: IFunc = function(a) {}

注意,如果使用接口來單獨描述一個函數,是沒 key

6.接口合併

多個同名的接口合併成一個接口

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10}

  • 如果合併的接口存在同名的非函數成員,則必須保證他們類型一致,否則編譯報錯
  • 接口中的同名函數則是採用重載(在函數詳解中會進一步講解)

四、高級類型

1.聯合類型

聯合類型也可以稱爲多選類型,當我們希望標註一個變量爲多個類型之一時可以選擇聯合類型標註, 的關係

function css(ele: Element, attr: string, value: string|number) {
    // ...
}

let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一個聯合類型
if (box) {
    // ts 會提示有 null 的可能性,加上判斷更嚴謹
    css(box, 'width', '100px');
    css(box, 'opacity', 1);
    css(box, 'opacity', [1,2]);  // 錯誤
}

2.交叉類型

交叉類型也可以稱爲合併類型,可以把多種類型合併到一起成爲一種新的類型,並且 的關係

對一個對象進行擴展:

interface o1 {x: number, y: string};
interface o2 {z: number};

let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});

小技巧

TypeScript 在編譯過程中只會轉換語法(比如擴展運算符,箭頭函數等語法進行轉換,對於 API 是不會進行轉換的(也沒必要轉換,而是引入一些擴展庫進行處理的),如果我們的代碼中使用了 target 中沒有的 API ,則需要手動進行引入,默認情況下 TypeScript 會根據 target 載入核心的類型庫

targetes5 時: ["dom", "es5", "scripthost"]

targetes6 時: ["dom", "es6", "dom.iterable", "scripthost"]

如果代碼中使用了這些默認載入庫以外的代碼,則可以通過 lib 選項來進行設置

http://www.typescriptlang.org/docs/handbook/compiler-options.html

3.字面量類型

有的時候,我們希望標註的不是某個類型,而是一個固定值,就可以使用字面量類型,配合聯合類型會更有用

function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
  	// ...
}

// ok
box && setPosition(box, 'bottom');
// error
box && setPosition(box, 'hehe');

4.類型別名

有的時候類型標註比較複雜,這個時候我們可以類型標註起一個相對簡單的名字

type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
  	// ...
}

使用類型別名定義函數類型

這裏需要注意一下,如果使用 type 來定義函數類型,和接口有點不太相同

type callback = (a: string) => string;
let fn: callback = function(a) {};

// 或者直接
let fn: (a: string) => string = function(a) {}

interface 與 type 的區別

interface

  • 只能描述 object/class/function 的類型
  • 同名 interface 自動合併,利於擴展

type

  • 不能重名
  • 能描述所有數據

5.類型推導

每次都顯式標註類型會比較麻煩,TypeScript 提供了一種更加方便的特性:類型推導。TypeScript 編譯器會根據當前上下文自動的推導出對應的類型標註,這個過程發生在:

  • 初始化變量
  • 設置函數默認參數值
  • 返回函數值
// 自動推斷 x 爲 number
let x = 1;
// 不能將類型“"a"”分配給類型“number”
x = 'a';

// 函數參數類型、函數返回值會根據對應的默認值和返回值進行自動推斷
function fn(a = 1) {return a * a}

6.類型斷言

有的時候,我們可能標註一個更加精確的類型(縮小類型標註範圍),比如:

let img = document.querySelector('#img');

我們可以看到 img 的類型爲 Element,而 Element 類型其實只是元素類型的通用類型,如果我們去訪問 src 這個屬性是有問題的,我們需要把它的類型標註得更爲精確:HTMLImageElement 類型,這個時候,我們就可以使用類型斷言,它類似於一種 類型轉換:

let img = <HTMLImageElement>document.querySelector('#img');

或者

let img = document.querySelector('#img') as HTMLImageElement;


注意:斷言只是一種預判,並不會數據本身產生實際的作用,即:類似轉換,但並非真的轉換了

五、函數詳解

1.函數的標註

一個函數的標註包含

  • 參數
  • 返回值
function fn(a: string): string {};
let fn: (a: string) => string = function(a) {};

type callback = (a: string): string;
interface ICallBack {
  (a: string): string;
}

let fn: callback = function(a) {};
let fn: ICallBack = function(a) {};

2.可選參數和默認參數

可選參數

通過參數名後面添加 ? 來標註該參數是可選的

let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {

}
// 設置
div && css( div, 'width', '100px' );
// 獲取
div && css( div, 'width' );

默認參數

我們還可以給參數設置默認值

  • 有默認值的參數也是可選的
  • 設置了默認值的參數可以根據值自動推導類型
function sort(items: Array<number>, order = 'desc') {}
sort([1,2,3]);

// 也可以通過聯合類型來限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');

3.剩餘參數

剩餘參數是一個數組,所以標註的時候一定要注意

interface IObj {
    [key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
    return others.reduce( (prev, currnet) => {
        prev = Object.assign(prev, currnet);
        return prev;
    }, target );
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});

4.函數中的 this

無論是 JavaScript 還是 TypeScript ,函數中的 this 都是我們需要關心的,那函數中 this 的類型該如何進行標註呢?

  • 普通函數
  • 箭頭函數

普通函數

對於普通函數而言,this 是會隨着調用環境的變化而變化的,所以默認情況下,普通函數中的 this 被標註爲 any,但我們可以在函數的第一個參數位(它不佔據實際參數位置)上顯式的標註 this 的類型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj1:T = {
    a: 1,
    fn(x: number) {
        //any類型
        console.log(this);
    }
}


let obj2:T = {
    a: 1,
    fn(this: T, x: number) {
        //通過第一個參數位標註 this 的類型,它對實際參數不會有影響
        console.log(this);
    }
}
obj2.fn(1);

箭頭函數

箭頭函數的 this 不能像普通函數那樣進行標註,它的 this 標註類型取決於它所在的作用域 this 的標註類型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj2: T = {
    a: 2,
    fn(this: T) {
        return () => {
            // T
            console.log(this);
        }
    }
}

5.函數重載

有的時候,同一個函數會接收不同類型的參數返回不同類型的返回值,我們可以使用函數重載來實現,通過下面的例子來體會一下函數重載

function showOrHide(ele: HTMLElement, attr: string, value: 'block'|'none'|number) {
	//
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
	// error,這裏是有問題的,雖然通過聯合類型能夠處理同時接收不同類型的參數,但是多個參數之間是一種組合的模式,我們需要的應該是一種對應的關係
  showOrHide( div, 'display', 1 );
}

我們來看一下函數重載

function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
function showOrHide(ele: HTMLElement, attr: string, value: any) {
  ele.style[attr] = value;
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
  // 通過函數重載可以設置不同的參數對應關係
  showOrHide( div, 'display', 1 );
}

  • 重載函數類型只需要定義結構,不需要實體,類似接口
interface PlainObject {
    [key: string]: string|number;
}

function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
    if (typeof attr === 'string' && value) {
        ele.style[attr] = value;
    }
    if (typeof attr === 'object') {
        for (let key in attr) {
            ele.style[attr] = attr[key];
        }
    }
}

let div = document.querySelector('div');
if (div) {
    css(div, 'width', '100px');
    css(div, {
        width: '100px'
    });

    // error,如果不使用重載,這裏就會有問題了
    css(div, 'width');
}


六、面向對象編程

1.類

classECMAScript6 中新增的語法,用於定義一個 ,在 TypeScript 中也有,並且有更多特性

2.類的基礎

ECMAScript 中的類語法結構基本類似

  • class 關鍵字
  • 構造函數:constructor
  • 成員屬性定義
  • 成員方法
  • this關鍵字

我們重點要說的是與 ECMAScript 中不同的點

3.什麼是類

對象 : 對某種事物所擁有的特徵和行爲進行的一種結構化描述

interface User {
  id: number;
  username: string;
  password: string;
  postArticle(title: string, content: string): void;
}
// 通過 key / value 結構描述一個對象(一個用戶)
let user1 = {
  id: 1,
  username: 'wXin',
  password: '123456',
  postArticle(title: string, content: string) {
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}
let user2 = {
  id: 2,
  username: 'WxIN2',
  password: '654321',
  postArticle(title: string, content: string) {
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

: 對一類具有相同特性事物的抽象描述,通過 class 來描述一個類,組織類的結構

class User {
  
}

4.成員屬性與方法定義

class User {
  id: number;
  username: string;
	password: string;
	
	postArticle(title: string, content: string): void {
    // 在類的內部可以通過 `this` 來訪問成員屬性和方法
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

ECMAScript7 之前,類的成員屬性是在構造函數中進行初始化的

5.構造函數

通過 new 運算符 + 類名,可以創建一個該類所描述的對象,我們稱這個過程爲:實例化

let user1 = new User;
let user2 = new User;

當我們 new User 的時候,會自動調用該類下的一個名爲 constructor 的方法,如果沒有顯式定義該方法,則會自動創建一個無參的 constructor 的空方法

class User {
	
	constructor() {}
  
}

注意:構造函數 constructor 不允許有返回值類型標註

class User {
  id: number;
  username: string;
	password: string;
  
  constructor(id: number, username: string, password: string) {
    this.id = id;
    this.username = username;
    this.password = password;
  }
	
	postArticle(title: string, content: string): void {
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'wXin', '123456');
let user2 = new User(2, 'WxIN2', '654321');

構造函數參數屬性

我們可以給構造函數參數添加修飾符來直接生成成員屬性

class User {
  
  constructor(
  	public id: number,
    public username: string,
    public password: string
  ) {
    // 可以省略初始化賦值
  }
	
	postArticle(title: string, content: string): void {
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'wXin', '123456');
let user2 = new User(2, 'WxIN2', '654321');

6.繼承

我們可以通過 extends 關鍵字來實現類的繼承

class VIP extends User {
  
}

super 關鍵字

在子類中,我們可以通過 super 來引用父類

  • 如果子類有自己的構造函數,則需要在子類構造函數中顯示的調用父類構造函數 : super(//參數),否則會報錯
  • 在子類構造函數中只有在 super(//參數) 之後才能訪問 this
  • 如果子類沒有重寫構造函數,則會在默認的 constructor 中無參調用 super()
  • 在子類中,可以通過 super 來訪問父類的成員屬性和方法
  • 通過 super 訪問父類的的同時,會自動綁定上下文對象爲當前子類 this
class VIP extends User {
  
  constructor(
  		id: number,
      username: string,
      password: string,
      public allowFileTypes = ['png','gif','jpg']
    ) {
        super(id, username, password);
    }
  
  postAttachment(file: File): void {
    console.log(`${this.username} 上傳了一個附件: ${file.name}`)
  }
}

let vip1 = new VIP(1, 'wXin', '123456');
let fileElement = <HTMLInputElement>document.querySelector('input[type="file"]');
let file = fileElement.files && fileElement.files[0];
file && vip1.postAttachment(file);


方法重載

class VIP extends User {
  
    constructor(
  		id: number,
      username: string,
      password: string,
      public allowFileTypes = ['png','gif','jpg']
    ) {
        super(id, username, password);
    }
    
  	// postArticle 方法重載
    postArticle(title: string, content: string, file?: File): void {
      	// 通過 super 調用父類實例方法
        super.postArticle(title, content);
        file && this.postAttachment(file);
    }
    
    postAttachment(file: File): void {
        console.log(`${this.username} 上傳了一個附件: ${file.name}`)
    }
}

// 具體使用場景
let vip1 = new VIP(1, 'wXin', '123456');
let fileElement = document.querySelector('input[type="file"]') as HTMLInputElement;
let buttonElement = document.querySelector('button') as HTMLButtonElement;

buttonElement.onclick = function() {
    // vip1.postArticle('標題一', '內容一');
    let file;
    if (fileElement.files) {
        file = fileElement.files[0];
        // vip1.postAttachment(file);
    }
    vip1.postArticle('標題一', '內容一', file);
}


7.修飾符

有的時候,我們希望對類成員(屬性、方法)進行一定的訪問控制,來保證數據的安全,通過 類修飾符 可以做到這一點,目前 TypeScript 提供了四種修飾符:

  • public:公有,默認
  • protected:受保護
  • private:私有
  • readonly:只讀

public 修飾符

這個是類成員的默認修飾符,它的訪問級別爲:

  • 自身
  • 子類
  • 類外

protected 修飾符

它的訪問級別爲:

  • 自身
  • 子類

private 修飾符

它的訪問級別爲:

  • 自身

readonly 修飾符

只讀修飾符只能針對成員屬性使用,且必須在聲明時或構造函數裏被初始化,它的訪問級別爲:

  • 自身
  • 子類
  • 類外
class User {
  
  constructor(
  	// 可以訪問,但是一旦確定不能修改
  	readonly id: number,
    // 可以訪問,但是不能外部修改
    protected username: string,
    // 外部包括子類不能訪問,也不可修改
    private password: string
  ) {
    // ...
  }
	// ...
}

let user1 = new User(1, 'wXin', '123456');


8.寄存器

有的時候,我們需要對類成員 屬性 進行更加細膩的控制,就可以使用 寄存器 來完成這個需求,通過 寄存器,我們可以對類成員屬性的訪問進行攔截並加以控制,更好的控制成員屬性的設置和訪問邊界,寄存器分爲兩種:

  • getter
  • setter

getter

訪問控制器,當訪問指定成員屬性時調用

setter

設置控制器,當設置指定成員屬性時調用

class User {
    private _id: number;
    private _username: string;
    private _password: string;
    
    constructor(id: number, username: string, password: string) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public set id(id: number) {
        this._id = id;
    }

    public get id() {
        return this._id;
    }

    public set username(username: string) {
        this._username = username;
    }

    public get username() {
        return this._username;
    }

    public set password(password: string) {
        if (password.length >= 6) {
            this._password = password;
        }
    }

    public get password() {
        return '******';
    }
  	// ...
}


9.靜態成員

前面我們說到的是成員屬性和方法都是實例對象的,但是有的時候,我們需要給類本身添加成員

type allow_file_type_list = 'png'|'gif'|'jpg'|'jpeg'|'webp';

class VIP extends User {
  
  // static 必須再 readonly 之前
  static readonly ALLOW_FILE_TYPE_LIST: Array<allow_file_type_list> = ['png','gif','jpg','jpeg','webp'];
  
  private _allowFileTypes: Array<allow_file_type_list>;
  
  constructor(
  	  id: number,
      username: string,
      password: string,
      allowFileTypes: Array<allow_file_type_list> = ['png','gif','jpg']
    ) {
        super(id, username, password);
        this._allowFileTypes = allowFileTypes;
    }
  
  public set allowFileTypes(types: Array<allow_file_type_list>) {
    this._allowFileTypes = types;
  }

  public get allowFileTypes() {
    return this._allowFileTypes;
  }

  public addType(type: allow_file_type_list) {
    this._allowFileTypes.push(type);
  }
}

let vip1 = new VIP(1, 'wXin', '123456', ['jpg','jpeg']);
// vip1
console.log(vip1.allowFileTypes);
let vip2 = new VIP(2, 'cc', '654321');
// vip1
console.log(vip2.allowFileTypes);
// 所有 VIP 可以設置的附件類型
console.log(VIP.ALLOW_FILE_TYPE_LIST);


  • 類的靜態成員是屬於類的,所以不能通過實例對象(包括 this)來進行訪問,而是直接通過類名訪問(不管是類內還是類外)
  • 靜態成員也可以通過訪問修飾符進行修飾
  • 靜態成員屬性一般約定(非規定)全大寫

10.抽象類

有的時候,一個基類(父類)的一些方法無法確定具體的行爲,而是由繼承的子類去實現,看下面的例子,

現在要通過一個類來美化系統的 MessageBox,它包含了:alert、confirm 和 prompt,設計結構如下:

// MessageBox
class MessageBox {
    constructor(){}
    show(){}
    close(){}
    
    // 注意這裏,對於alert、confirm 和 prompt,它們有自己不同的內容,所以MessageBox無法去確定setContent的具體行爲
    setContent(content: string){}
}
// alert
class Alert extends MessageBox {
    constructor(){
        super()
    }
    // 重寫
    setContent(content: string){
        // 內容+一個確定按鈕
    }
}
// confirm
class Confirm extends MessageBox {
    constructor(){
        super()
    }
    // 重寫
    setContent(content: string){
        // 內容+一個確定按鈕+一個取消按鈕
    }
}
//prompt
class Prompt extends MessageBox {
    constructor(){
        super()
    }
    // 重寫
    setContent(content: string){
        // 一個輸入框+一個確定按鈕+一個取消按鈕
    }
}


大家可以發現每個子類都重寫了父類的 setContent 方法,父類的 setContent 方法並不需要去實現什麼,這個時候我們可以抽象父類的 setContent 方法

abstract 關鍵字

如果一個方法沒有具體的實現方法,則可以通過 abstract 關鍵字進行修飾

// MessageBox
abstract class MessageBox {
    constructor(){}
    show(){}
    close(){}
    
    // 注意這裏,對於alert、confirm 和 prompt,它們有自己不同的內容,所以MessageBox無法去確定setContent的具體行爲
    abstract setContent(content: string): void
}


使用抽象類有一個好處:

約定了所有繼承子類的所必須實現的方法,使類的涉及更加的規範

這裏需要注意:

  • abstract 修飾的方法不能有方法體
  • 如果一個類有抽象方法,那麼該類也必須爲抽象的
  • 如果一個類是抽象的,那麼就不能使用 new 進行實例化(因爲抽象類表名該類有未實現的方法,所以不允許實例化)
  • 如果一個子類繼承了一個抽象類,那麼該子類就必須實現抽象類中的所有抽象方法,否則該類還得聲明爲抽象的

11.類與接口

在前面我們已經學習了接口的使用,通過接口,我們可以爲對象定義一種結構和契約。我們還可以把接口與類進行結合,通過接口,讓類去強制符合某種契約,從某個方面來說,當一個抽象類中只有抽象的時候,它就與接口沒有太大區別了,但是 類會產生實體代碼,接口不會

  • 一個類使用 implements 關鍵字來確定要實現的接口,當一個類 implements 了某個接口,那麼該類必須實現接口中定義的結構
// 數據格式
interface SpreadSheetData {
    name: string;
    description: string;
}

// 定義一個SpreadSheet接口
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}

// 用戶
class User implements SpreadSheetInfo {

    constructor(
        private id: number,
        private name: string,
        private gender: string
    ) {
        
    }

    getInfo() {
        return {
            name: this.name,
            description: `我叫 ${this.name},性別 ${this.gender}`
        }
    }
}

// 課目
class Course implements SpreadSheetInfo {

    constructor(
        private id: number,
        private type: string,
        private title: string,
        private price: number
    ) {

    }

    getInfo() {
        return {
            name: this.title,
            description: `${this.type} 新課程 ${this.title},只要 ${this.price}`
        }
    }

}

// 電子表格
class SpreadSheet {

    private _datas: Array<SpreadSheetData>

    public get datas() {
        return this._datas;
    }

    add(origin: SpreadSheetInfo) {
        this._datas.push( origin.getInfo() );
    }

}



let spreadSheet = new SpreadSheet();

let user1 = new User(1, 'Wxin', '男');
let user2 = new User(1, 'cc', '男');

let course1 = new Course(1, 'js', 'vue', 1000);
let course2 = new Course(2, 'js', 'react', 1000);

spreadSheet.add( user1 );
spreadSheet.add( user2 );
spreadSheet.add( course1 );
spreadSheet.add( course2 );


  • TypeScript 只支持單繼承,不支持繼承多個父類,而一個類可以實現多個接口,多個接口使用 , 分隔
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}
interface IStorage extends ILogger {
    save(data: string): void;
}


  • 接口也可以繼承
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}
// IStorage
interface IStorage extends ILogger {
    save(data: string): void;
}


12.類與對象類型

當我們在 TypeScript 定義一個類的時候,其實同時定義了兩個不同的類型

  • 類類型(構造函數類型)
  • 對象類型

首先,對象類型好理解,就是我們的 new 出來的實例類型

那類類型是什麼,我們知道 JavaScript 中的類,或者說是 TypeScript 中的類其實本質上還是一個函數,當然我們也稱爲構造函數,那麼這個類或者構造函數本身也是有類型的,那麼這個類型就是類的類型

class Person {
	// 屬於類的
  static type = '人';

  // 屬於實例的
  name: string;
  age: number;
  gender: string;

  // 類的構造函數也是屬於類的
  constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

}

let p1 = new Person('wx', 26, '男');

let Person2: typeof Person = Person;
console.log(Person2.type);


封裝一個工廠函數

function createInstance(constructor: Person): Person {
  // 這是有錯誤的,因爲 Person 表示的 new 出來的實例的類型,而不是構造函數(類)的類型
  return new constructor('wx', 26, '男');
}


正確的做法

interface PersonConstructor {
    new (name: string, age: number, gender: '男'|'女'): Person;
}
function createInstance(constructor: PersonConstructor): Person {
    return new constructor('wx', 26, '男');
}


或者

type PersonConstructor = typeof Person;
function createInstance(constructor: PersonConstructor): Person {
    return new constructor('wx', 26, '男');
}


注意上面的 typeof Person,我們就是通過 typeof 來獲取這個類的類類型,這裏的 typeofJavaScript 中的 typeof 有一定的差異性,後續我們會講到

七、泛型

1.爲什麼要使用泛型

許多時候,標註的具體類型並不能確定,比如一個函數的參數類型

function sort(items: Array<string>, order: 'desc'|'asc') {
    //...
}

上面的 sort 函數雖然標註了類型,但是同時也限制了傳入的只能是字符串數組。實際上,我們是希望它能傳入字符串數組、數字數組、甚至用戶自定義的結構類型,這個時候我們希望傳入的類型能在具體調用的時候再確定,就像是函數內部可變數據轉爲參數一樣。泛型 - 就可以完成這個需求

2.泛型的使用

function sort<T>(items: T, order: 'desc'|'asc'): T {
    //...
}

所謂的泛型,就是給可變(不定)的類型定義變量(參數),<> 類似 ()

3.泛型接口

我們還可以在接口中使用泛型

場景

後端提供了一些接口,用以返回一些數據,依據返回的數據格式定義如下接口:

interface IResponseData {
    code: number;
    message?: string;
    data: any;
}

我們會發現該接口的 data 項的具體格式不確定,不同的接口會返回的數據是不一樣的

// 用戶接口
interface IResponseUserData {
    id: number;
    username: string;
    email: string;
}
// 文章接口
interface IResponseArticleData {
    id: number;
    title: string;
    author: IResponseUserData;
}

這個時候我們可以對 IResponseData 使用泛型

interface IResponseData<T> {
    code: number;
    message?: string;
    data: T;
}

下面是具體代碼

function getUser<U>(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData<U>) => {
        return data;
    });
}
function getArticles<U>(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData<U>) => {
        return data;
    } )
}

~(async function(){
    let user = await getUser<IResponseUserData>('');
    if (user.code === 1) {
        console.log(user.message);
    } else {
        console.log(user.data.username);
    }

    let articles = await getArticles<IResponseArticleData>('');
    if (articles.code === 1) {
        console.log(articles.message);
    } else {
        console.log(articles.data.id);
        console.log(articles.data.author.username);
    }
});

4.泛型類

還可以這類中使用泛型

class Queue<T> {
    private items: Array<T> = [];

    add(item: T) {
        this.items.push(item);
    }

    remove(): T | undefined {
        return this.items.shift();
    }
}

let q1 = new Queue<string>();
q1.add('a');
q1.add('b');
let v = q1.remove();
if (v) {
    v.substring(0);
}

let q2 = new Queue<Element>();

let box = document.querySelector('.box');
let div = document.querySelector('div');
box && q2.add(box);
div && q2.add(div);
let v2 = q2.remove();
if (v2) {
    v2.classList.add('box')
}

八、裝飾器

1.什麼是裝飾器

裝飾器-DecoratorsTypeScript 中是一種可以在不修改類代碼的基礎上通過添加標註的方式來對類型進行擴展的一種方式

  • 提高代碼複用率
  • 減少代碼量
  • 提高代碼擴展性、可讀性和維護性

TypeScript 中,裝飾器只能在類中使用

2.功能擴展

現有一個 M 類,實現了兩個用於加、減的方法:addsub

// 原始類
class M {
    static add(a: number, b: number) {
        return a + b;
    }
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1,2);
console.log(v1);
let v2 = M.sub(1,2);
console.log(v2);

需求:每次調用 addsub 的時候,同時保存(如:localStorage)參或者打印輸出日誌(如:console.log())與計算的數據與結果

// 原始類
class M {
    static add(a: number, b: number) {
      	let result = a + b;
      	log('add', a, b, result);
        return result;
    }
    static sub(a: number, b: number) {
				let result = a + b;
      	log('sub', a, b, result);
        return result;
    }
}

function storageData(type: string, a: number, b: number, result: number) {
  	console.log({
      type,
      a,
      b,
      result
    })
}

let v1 = M.add(1,2);
console.log(v1);
let v2 = M.sub(1,2);
console.log(v2);

上述方式雖然很快的實現了需求。但是,這樣的做法對原有代碼照成了破壞和侵入式的修改,不利於代碼的維護。

storageData 抽離出來,通過 storageData 來包裝方法

// 原始類
class M {
    static add(a: number, b: number) {
      	return a + b;
    }
    static sub(a: number, b: number) {
				return a - b;
    }
}

function storageData(fn: Function, type: string, a: number, b: number) {
  	let result = fn(a, b);
  	console.log({
      type,
      a,
      b,
      result
    })
  	return result;
}

let v1 = log(M.add, 'add', 1, 2);
console.log(v1);
let v2 = log(M.sub, 'sub', 1, 2);
console.log(v2);

這樣做雖然可以避免去修改 addsub 方法,但是我們又得去大量的修改調用代碼,怎樣才能在不對代碼進行修改也不對調用進行修改的同時來進行功能的擴展呢,這就是:裝飾器 - Decorator

3.裝飾器語法

裝飾器的使用及其的簡單

  • 裝飾器本質就是一個函數,如上面的 storageData 就是
  • 通過特定語法在特定的位置調用裝飾器函數即可對數據(類、方法、甚至參數等)進行擴展
// 裝飾器函數
function log(target: Function, type: string, descriptor: PropertyDescriptor) {
    let value = descriptor.value;

    descriptor.value = function(a: number, b: number) {
        let result = value(a, b);
        console.log('日誌:', {
            type,
            a,
            b,
            result
        })
        return result;
    }
}

// 原始類
class M {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

4.裝飾器

裝飾器 是一個函數,它可以通過 @裝飾器函數 這種特殊的語法附加在 方法訪問符屬性參數 上,對它們進行包裝,然後返回一個包裝後的目標對象(方法訪問符屬性參數 ),裝飾器工作在類的構建階段,而不是使用階段

function 裝飾器1() {}
...

@裝飾器1
class MyClass {
  
  private _x: number;
  
  @裝飾器2
  property1: number;
  
  @裝飾器3
  get x() { return this._x; }
  
  @裝飾器4
  public method1(@裝飾器5 x: number) {
    //
  }
}

類裝飾器

目標

  • 應用於類的構造函數

參數

  • 第一個參數(也只有一個參數)
    • 類的構造函數作爲其唯一的參數

方法裝飾器

目標

  • 應用於類的方法上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 方法名稱
  • 第三個參數
    • 方法描述符對象

屬性裝飾器

目標

  • 應用於類的屬性上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 屬性名稱

訪問器裝飾器

目標

  • 應用於類的訪問器(getter、setter)上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 屬性名稱
  • 第三個參數
    • 方法描述符對象

參數裝飾器

目標

  • 應用在參數上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 方法名稱
  • 第三個參數
    • 參數在函數參數列表中的索引

5.裝飾器執行順序

實例裝飾器

​ 屬性 => 訪問符 => 參數 => 方法

靜態裝飾器

​ 屬性 => 訪問符 => 參數 => 方法

​ 類

6.裝飾器工廠

如果我們需要給裝飾器執行過程中傳入一些參數的時候,就可以使用裝飾器工廠來實現

// 裝飾器函數
function log(callback: Function) {
  	return function(target: Function, type: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
            callback({
                type,
                a,
                b,
                result
            });
            return result;
        }
    }
}

// 原始類
class M {
    @log(function(result: any) {
      	console.log('日誌:', result)
    })
    static add(a: number, b: number) {
        return a + b;
    }
    @log(function(result: any) {
      	localStorage.setItem('log', JSON.stringify(result));
    })
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

7.元數據

裝飾器 函數中 ,我們可以拿到 方法訪問符屬性參數 的基本信息,如它們的名稱,描述符 等,但是我們想獲取更多信息就需要通過另外的方式來進行:元數據

什麼是元數據?

元數據 :用來描述數據的數據,在我們的程序中,對象 等都是數據,它們描述了某種數據,另外還有一種數據,它可以用來描述 對象,這些用來描述數據的數據就是 元數據

比如一首歌曲本身就是一組數據,同時還有一組用來描述歌曲的歌手、格式、時長的數據,那麼這組數據就是歌曲數據的元數據

使用 reflect-metadata

https://www.npmjs.com/package/reflect-metadata

首先,需要安裝 reflect-metadata

npm install reflect-metadata

定義元數據

我們可以 方法 等數據定義元數據

  • 元數據會被附加到指定的 方法 等數據之上,但是又不會影響 方法 本身的代碼

設置

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

  • metadataKey:meta 數據的 key
  • metadataValue:meta 數據的 值
  • target:meta 數據附加的目標
  • propertyKey:對應的 property key

調用方式

  • 通過 Reflect.defineMetadata 方法調用來添加 元數據
  • 通過 @Reflect.metadata 裝飾器來添加 元數據
@Reflect.metadata("name", '我是A類')
class A {
    @Reflect.metadata("name1", "val1")
    public method1() {
    }
   
  	@Reflect.metadata("name2", "val2")
  	public method2() {
    }
}

// or
Reflect.defineMetadata("name", "我是A類", A);
Reflect.defineMetadata("name1", "val1", new A, 'method1');
Reflect.defineMetadata("name2", "val2", new A, 'method2');

獲取

Reflect.getMetadata(metadataKey, target, propertyKey)

參數的含義與 defineMetadata 對應

使用 emitDecoratorMetadata

tsconfig.json 中有一個配置 emitDecoratorMetadata,開啓該特性,typescript 會在編譯之後自動給 方法訪問符屬性參數 添加如下幾個元數據

  • design:type:被裝飾目標的類型
    • 成員屬性:屬性的標註類型
    • 成員方法:Function 類型
  • design:paramtypes
    • 成員方法:方法形參列表的標註類型
    • 類:構造函數形參列表的標註類型
  • design:returntype
    • 成員方法:函數返回值的標註類型
import "reflect-metadata"

function n(target: any) {
}
function f(name: string) {
    return function(target: any, propertyKey: string, descriptor: any) {
      	console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
        console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
        console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
    }
}
function m(target: any, propertyKey: string) {

}

@n
class B {
    @m
    name: string;

    constructor(a: string) {

    }

    @f('')
    method1(a: string, b: string) {
        return 'a'
    }
}


編譯後

__decorate([
    m,
    __metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
    f(''),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
    n,
    __metadata("design:paramtypes", [String])
], B);


發佈了16 篇原創文章 · 獲贊 6 · 訪問量 9673
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章