做爲一門強大的靜態類型檢查工具,現在在許多中大型應用程序以及流行的JS庫中均能看到TypeScript的身影。JS做爲一門弱類型語言,在咱們寫代碼的過程當中稍不留神便會修改掉變量的類型,從而致使一些出乎意料的運行時錯誤。然而TypeScript在編譯過程當中便能幫咱們解決這個難題,不只在JS中引入了強類型檢查,而且編譯後的JS代碼可以運行在任何瀏覽器環境,Node環境和任何支持ECMAScript 3(或更高版本)的JS引擎中。最近公司恰好準備使用TypeScript來對現有系統進行重構,之前使用TypeScript的機會也很少,特別是一些有用的高級用法,因此藉着此次機會,從新鞏固夯實一下這方面的知識點,若是有錯誤的地方,還請指出。javascript
在ES5中,咱們通常經過函數或者基於原型的繼承來封裝一些組件公共的部分方便複用,然而在TypeScript中,咱們能夠像相似Java語言中以面向對象的方式使用類繼承來建立可複用的組件。咱們能夠經過class
關鍵字來建立類,並基於它使用new
操做符來實例化一個對象。爲了將多個類的公共部分進行抽象,咱們能夠建立一個父類並讓子類經過extends
關鍵字來繼承父類,從而減小一些冗餘代碼的編寫增長代碼的可複用性和可維護性。示例以下:前端
class Parent { readonly x: number; constructor() { this.x = 1; } print() { console.log(this.x); } } class Child extends Parent { readonly y: number; constructor() { // 注意此處必須優先調用super()方法 super(); this.y = 2; } print() { // 經過super調用父類原型上的方法,可是方法中的this指向的是子類的實例 super.print(); console.log(this.y); } } const child = new Child(); console.log(child.print()) // -> 1 2
在上述示例中,Child
子類中對父類的print
方法進行重寫,同時在內部使用super.print()
來調用父類的公共邏輯,從而實現邏輯複用。class
關鍵字做爲構造函數的語法糖,在通過TypeScript編譯後,最終會被轉換爲兼容性好的瀏覽器可識別的ES5代碼。class
在面向對象的編程範式中很是常見,所以爲了弄清楚其背後的實現機制,咱們不妨多花點時間來看下通過編譯轉換以後的代碼是什麼樣子的(固然這部分已經比較熟悉的同窗能夠直接跳過)。java
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Parent = /** @class */ (function () { function Parent() { this.x = 1; } Parent.prototype.print = function () { console.log(this.x); }; return Parent; }()); var Child = /** @class */ (function (_super) { __extends(Child, _super); function Child() { var _this = // 注意此處必須優先調用super()方法 _super.call(this) || this; _this.y = 2; return _this; } Child.prototype.print = function () { // 經過super調用父類原型上的方法,可是方法中的this指向的是子類的實例 _super.prototype.print.call(this); console.log(this.y); }; return Child; }(Parent)); var child = new Child(); console.log(child.print()); // -> 1 2
以上就是轉換後的完整代碼,爲了方便對比,這裏將原來的註釋信息保留,仔細研究這段代碼咱們會發現如下幾個要點:
1) 子類Child
的構造函數中super()
方法被轉換成了var _this = _super.call(this) || this
,這裏的_super
指的就是父類Parent
,所以這句代碼的含義就是調用父類構造函數並將this
綁定到子類的實例上,這樣的話子類實例即可擁有父類的x
屬性。所以爲了實現屬性繼承,咱們必須在子類構造函數中調用super()
方法,若是不調用會編譯不經過。git
2) 子類Child
的print
方法中super.print()
方法被轉換成了_super.prototype.print.call(this)
,這句代碼的含義就是調用父類原型上的print
方法並將方法中的this
指向子類實例,因爲在上一步操做中咱們已經繼承到父類的x
屬性,所以這裏咱們將直接打印出子類實例的x
屬性的值。github
3) extends
關鍵字最終被轉換爲__extends(Child, _super)
方法,其中_super
指的是父類Parent
,爲了方便查看,這裏將_extends
方法單獨提出來進行研究。編程
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); } return function (d, b) { // 第一部分 extendStatics(d, b); // 第二部分 function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })();
在以上代碼中,主要能夠分爲兩個部分來進行理解,第一部分爲extendStatics(d, b)
方法,第二部分爲該方法後面的兩行代碼。瀏覽器
第一部分
:ide
在extendStatics
方法內部雖然代碼量相對較多,可是不難發現其實仍是主要爲了兼容ES5版本的執行環境。在ES6中新增了Object.setPrototypeOf
方法用於手動設置對象的原型,可是在ES5的環境中咱們通常經過一個非標準的__proto__
屬性來進行設置,Object.setPrototypeOf
方法的原理其實也是經過該屬性來設置對象的原型,其實現方式以下:函數
Object.setPrototypeOf = function(obj, proto) { obj.__proto__ = proto; return obj; }
在extendStatics(d, b)
方法中,d
指子類Child
,b
指父類Parent
,所以該方法的做用能夠解釋爲:工具
// 將子類Child的__proto__屬性指向父類Parent Child.__proto__ = Parent;
能夠將這行代碼理解爲構造函數的繼承,或者叫靜態屬性和靜態方法的繼承,即屬性和方法不是掛載到構造函數的prototype
原型上的,而是直接掛載到構造函數自己,由於在JS中函數自己也能夠做爲一個對象,並能夠爲其賦予任何其餘的屬性,示例以下:
function Foo() { this.x = 1; this.y = 2; } Foo.bar = function() { console.log(3); } Foo.baz = 4; console.log(Foo.bar()) // -> 3 console.log(Foo.baz) // -> 4
所以當咱們在子類Child
中以Child.someProperty
訪問屬性時,若是子類中不存在就會經過Child.__proto__
尋找父類的同名屬性,經過這種方式來實現靜態屬性和靜態方法的路徑查找。
第二部分
:
在第二部分中僅包含如下兩行代碼:
function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
其中d
指子類Child
,b
指父類Parent
,這裏對於JS中實現繼承的幾種方式比較熟悉的同窗能夠一眼看出,這裏使用了寄生組合式繼承的方式,經過借用一箇中間函數__()
來避免當修改子類的prototype
上的方法時對父類的prototype
所形成的影響。咱們知道,在JS中經過構造函數實例化一個對象以後,該對象會擁有一個__proto__
屬性並指向其構造函數的prototype
屬性,示例以下:
function Foo() { this.x = 1; this.y = 2; } const foo = new Foo(); foo.__proto__ === Foo.prototype; // -> true
對於本例中,若是經過子類Child
來實例化一個對象以後,會產生以下關聯:
const child = new Child(); child.__proto__ === (Child.prototype = new __()); child.__proto__.__proto__ === __.prototype === Parent.prototype; // 上述代碼等價於下面這種方式 Child.prototype.__proto__ === Parent.prototype;
所以當咱們在子類Child
的實例child
對象中經過child.someMethod()
調用某個方法時,若是在實例中不存在該方法,則會沿着__proto__
繼續往上查找,最終會通過父類Parent
的prototype
原型,即經過這種方式來實現方法的繼承。
基於對以上兩個部分的分析,咱們能夠總結出如下兩點:
// 表示構造函數的繼承,或者叫作靜態屬性和靜態方法的繼承,老是指向父類 1. Child.__proto__ === Parent; // 表示方法的繼承,老是指向父類的prototype屬性 2. Child.prototype.__proto__ === Parent.prototype;
TypeScript爲咱們提供了訪問修飾符(Access Modifiers)來限制在class
外部對內部屬性的訪問,訪問修飾符主要包含如下三種:
public
:公共修飾符,其修飾的屬性和方法都是公有的,能夠在任何地方被訪問到,默認狀況下全部屬性和方法都是public
的。private
:私有修飾符,其修飾的屬性和方法在class
外部不可見。protected
:受保護修飾符,和private
比較類似,可是其修飾的屬性和方法在子類內部是被容許訪問的。咱們經過一些示例來對幾種修飾符進行對比:
class Human { public name: string; public age: number; public constructor(name: string, age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name, man.age); // -> tom 20 man.age = 21; console.log(man.age); // -> 21
在上述示例中,因爲咱們將訪問修飾符設置爲public
,所以咱們經過實例man
來訪問name
和age
屬性是被容許的,同時對age
屬性從新賦值也是容許的。可是在某些狀況下,咱們但願某些屬性是對外不可見的,同時不容許被修改,那麼咱們就可使用private
修飾符:
class Human { public name: string; private age: number; // 此處修改成使用private修飾符 public constructor(name: string, age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> Property 'age' is private and only accessible within class 'Human'.
咱們將age
屬性的修飾符修改成private
後,在外部經過man.age
對其進行訪問,TypeScript在編譯階段就會發現其是一個私有屬性並最終將會報錯。
注意:在TypeScript編譯以後的代碼中並無限制對私有屬性的存取操做。
編譯後的代碼以下:
var Human = /** @class */ (function () { function Human(name, age) { this.name = name; this.age = age; } return Human; }()); var man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> 20
使用private
修飾符修飾的屬性或者方法在子類中也是不容許訪問的,示例以下:
class Human { public name: string; private age: number; public constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); console.log(this.age); } } const woman = new Woman('Alice', 18); // -> Property 'age' is private and only accessible within class 'Human'.
在上述示例中因爲在父類Human
中age
屬性被設置爲private
,所以在子類Woman
中沒法訪問到age
屬性,爲了讓在子類中容許訪問age
屬性,咱們可使用protected
修飾符來對其進行修飾:
class Human { public name: string; protected age: number; // 此處修改成使用protected修飾符 public constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); console.log(this.age); } } const woman = new Woman('Alice', 18); // -> 18
當咱們將private
修飾符用於構造函數時,則表示該類不容許被繼承或實例化,示例以下:
class Human { public name: string; public age: number; // 此處修改成使用private修飾符 private constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); } } const man = new Human('Alice', 18); // -> Cannot extend a class 'Human'. Class constructor is marked as private. // -> Constructor of class 'Human' is private and only accessible within the class declaration.
當咱們將protected
修飾符用於構造函數時,則表示該類只容許被繼承,示例以下:
class Human { public name: string; public age: number; // 此處修改成使用protected修飾符 protected constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); } } const man = new Human('Alice', 18); // -> Constructor of class 'Human' is protected and only accessible within the class declaration.
另外咱們還能夠直接將修飾符放到構造函數的參數中,示例以下:
class Human { // public name: string; // private age: number; public constructor(public name: string, private age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> Property 'age' is private and only accessible within class 'Human'.
當咱們的項目中擁有不少不一樣的類時而且這些類之間可能存在某方面的共同點,爲了描述這種共同點,咱們能夠將其提取到一個接口(interface)中用於集中維護,並使用implements
關鍵字來實現這個接口,示例以下:
interface IHuman { name: string; age: number; walk(): void; } class Human implements IHuman { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } }
上述代碼在編譯階段能順利經過,可是咱們注意到在Human
類中包含constructor
構造函數,若是咱們想在接口中爲該構造函數定義一個簽名並讓Human
類來實現這個接口,看會發生什麼:
interface HumanConstructor { new (name: string, age: number); } class Human implements HumanConstructor { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } } // -> Class 'Human' incorrectly implements interface 'HumanConstructor'. // -> Type 'Human' provides no match for the signature 'new (name: string, age: number): any'.
然而TypeScript會編譯出錯,告訴咱們錯誤地實現了HumanConstructor
接口,這是由於當一個類實現一個接口時,只會對實例部分進行編譯檢查,類的靜態部分是不會被編譯器檢查的。所以這裏咱們嘗試換種方式,直接操做類的靜態部分,示例以下:
interface HumanConstructor { new (name: string, age: number); } interface IHuman { name: string; age: number; walk(): void; } class Human implements IHuman { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } } // 定義一個工廠方法 function createHuman(constructor: HumanConstructor, name: string, age: number): IHuman { return new constructor(name, age); } const man = createHuman(Human, 'tom', 18); console.log(man.name, man.age); // -> tom 18
在上述示例中經過額外建立一個工廠方法createHuman
並將構造函數做爲第一個參數傳入,此時當咱們調用createHuman(Human, 'tom', 18)
時編譯器便會檢查第一個參數是否符合HumanConstructor
接口的構造器簽名。
在聲明合併中最多見的合併類型就是接口了,所以這裏先從接口開始介紹幾種比較常見的合併方式。
示例代碼以下:
interface A { name: string; } interface A { age: number; } // 等價於 interface A { name: string; age: number; } const a: A = {name: 'tom', age: 18};
接口合併的方式比較容易理解,即聲明多個同名的接口,每一個接口中包含不一樣的屬性聲明,最終這些來自多個接口的屬性聲明會被合併到同一個接口中。
注意:全部同名接口中的非函數成員必須惟一,若是不惟一則必須保證類型相同,不然編譯器會報錯。對於函數成員,後聲明的同名接口會覆蓋掉以前聲明的同名接口,即後聲明的同名接口中的函數至關於一次重載,具備更高的優先級。
函數的合併能夠簡單理解爲函數的重載,即經過同時定義多個不一樣類型參數或不一樣類型返回值的同名函數來實現,示例代碼以下:
// 函數定義 function foo(x: number): number; function foo(x: string): string; // 函數具體實現 function foo(x: number | string): number | string { if (typeof x === 'number') { return (x).toFixed(2); } return x.substring(0, x.length - 1); }
在上述示例中,咱們對foo
函數進行屢次定義,每次定義的函數參數類型不一樣,返回值類型不一樣,最後一次爲函數的具體實現,在實現中只有在兼容到前面的全部定義時,編譯器纔不會報錯。
注意:TypeScript編譯器會優先從最開始的函數定義進行匹配,所以若是多個函數定義存在包含關係,則須要將最精確的函數定義放到最前面,不然將始終不會被匹配到。
類型別名聯合與接口合併有所區別,類型別名不會新建一個類型,只是建立一個新的別名來對多個類型進行引用,同時不能像接口同樣被實現(implements)
和繼承(extends)
,示例以下:
type HumanProperty = { name: string; age: number; gender: number; }; type HumanBehavior = { eat(): void; walk(): void; } type Human = HumanProperty & HumanBehavior; let woman: Human = { name: 'tom', age: 18, gender: 0, eat() { console.log('I can eat.'); }, walk() { console.log('I can walk.'); } } class HumanComponent extends Human { constructor(public name: string, public age: number, public gender: number) { this.name = name; this.age = age; this.gender = gender; } eat() { console.log('I can eat.'); } walk() { console.log('I can walk.'); } } // -> 'Human' only refers to a type, but is being used as a value here.
在TypeScript中的keyof
有點相似於JS中的Object.keys()
方法,可是區別在於前者遍歷的是類型中的字符串索引,後者遍歷的是對象中的鍵名,示例以下:
interface Rectangle { x: number; y: number; width: number; height: number; } type keys = keyof Rectangle; // 等價於 type keys = "x" | "y" | "width" | "height"; // 這裏使用了泛型,強制要求第二個參數的參數名必須包含在第一個參數的全部字符串索引中 function getRectProperty<T extends object, K extends keyof T>(rect: T, property: K): T[K] { return rect[property]; } let rect: Rectangle = { x: 50, y: 50, width: 100, height: 200 }; console.log(getRectProperty(rect, 'width')); // -> 100 console.log(getRectProperty(rect, 'notExist')); // -> Argument of type '"notExist"' is not assignable to parameter of type '"width" | "x" | "y" | "height"'.
在上述示例中咱們經過使用keyof
來限制函數的參數名property
必須被包含在類型Rectangle
的全部字符串索引中,若是沒有被包含則編譯器會報錯,能夠用來在編譯時檢測對象的屬性名是否書寫有誤。
在某些狀況下,咱們但願類型中的全部屬性都不是必需的,只有在某些條件下才存在,咱們就可使用Partial
來將已聲明的類型中的全部屬性標識爲可選的,示例以下:
// 該類型已內置在TypeScript中 type Partial<T> = { [P in keyof T]?: T[P] }; interface Rectangle { x: number; y: number; width: number; height: number; } type PartialRectangle = Partial<Rectangle>; // 等價於 type PartialRectangle = { x?: number; y?: number; width?: number; height?: number; } let rect: PartialRectangle = { width: 100, height: 200 };
在上述示例中因爲咱們使用Partial
將全部屬性標識爲可選的,所以最終rect
對象中雖然只包含width
和height
屬性,可是編譯器依舊沒有報錯,當咱們不能明確地肯定對象中包含哪些屬性時,咱們就能夠經過Partial
來聲明。
在某些應用場景下,咱們可能須要從一個已聲明的類型中抽取出一個子類型,在子類型中包含父類型中的部分或所有屬性,這時咱們可使用Pick
來實現,示例代碼以下:
// 該類型已內置在TypeScript中 type Pick<T, K extends keyof T> = { [P in K]: T[P] }; interface User { id: number; name: string; age: number; gender: number; email: string; } type PickUser = Pick<User, "id" | "name" | "gender">; // 等價於 type PickUser = { id: number; name: string; gender: number; }; let user: PickUser = { id: 1, name: 'tom', gender: 1 };
在上述示例中,因爲咱們只關心user
對象中的id
,name
和gender
是否存在,其餘屬性不作明確規定,所以咱們就可使用Pick
從User
接口中揀選出咱們關心的屬性而忽略其餘屬性的編譯檢查。
never
表示的是那些永不存在的值的類型,好比在函數中拋出異常或者無限循環,never
類型能夠是任何類型的子類型,也能夠賦值給任何類型,可是相反卻沒有一個類型能夠做爲never
類型的子類型,示例以下:
// 函數拋出異常 function throwError(message: string): never { throw new Error(message); } // 函數自動推斷出返回值爲never類型 function reportError(message: string) { return throwError(message); } // 無限循環 function loop(): never { while(true) { console.log(1); } } // never類型能夠是任何類型的子類型 let n: never; let a: string = n; let b: number = n; let c: boolean = n; let d: null = n; let e: undefined = n; let f: any = n; // 任何類型都不能賦值給never類型 let a: string = '123'; let b: number = 0; let c: boolean = true; let d: null = null; let e: undefined = undefined; let f: any = []; let n: never = a; // -> Type 'string' is not assignable to type 'never'. let n: never = b; // -> Type 'number' is not assignable to type 'never'. let n: never = c; // -> Type 'true' is not assignable to type 'never'. let n: never = d; // -> Type 'null' is not assignable to type 'never'. let n: never = e; // -> Type 'undefined' is not assignable to type 'never'. let n: never = f; // -> Type 'any' is not assignable to type 'never'.
與Pick
相反,Pick
用於揀選出咱們須要關心的屬性,而Exclude
用於排除掉咱們不須要關心的屬性,示例以下:
// 該類型已內置在TypeScript中 // 這裏使用了條件類型(Conditional Type),和JS中的三目運算符效果一致 type Exclude<T, U> = T extends U ? never : T; interface User { id: number; name: string; age: number; gender: number; email: string; } type keys = keyof User; // -> "id" | "name" | "age" | "gender" | "email" type ExcludeUser = Exclude<keys, "age" | "email">; // 等價於 type ExcludeUser = "id" | "name" | "gender";
在上述示例中咱們經過在ExcludeUser
中傳入咱們不須要關心的age
和email
屬性,Exclude
會幫助咱們將不須要的屬性進行剔除,留下的屬性id
,name
和gender
即爲咱們須要關心的屬性。通常來講,Exclude
不多單獨使用,能夠與其餘類型配合實現更復雜更有用的功能。
在上一個用法中,咱們使用Exclude
來排除掉其餘不須要的屬性,可是在上述示例中的寫法耦合度較高,當有其餘類型也須要這樣處理時,就必須再實現一遍相同的邏輯,不妨咱們再進一步封裝,隱藏這些底層的處理細節,只對外暴露簡單的公共接口,示例以下:
// 使用Pick和Exclude組合實現 type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; interface User { id: number; name: string; age: number; gender: number; email: string; } // 表示忽略掉User接口中的age和email屬性 type OmitUser = Omit<User, "age" | "email">; // 等價於 type OmitUser = { id: number; name: string; gender: number; }; let user: OmitUser = { id: 1, name: 'tom', gender: 1 };
在上述示例中,咱們須要忽略掉User
接口中的age
和email
屬性,則只須要將接口名和屬性傳入Omit
便可,對於其餘類型也是如此,大大提升了類型的可擴展能力,方便複用。
在本文中總結了幾種TypeScript的使用技巧,若是在咱們的TypeScript項目中發現有不少類型聲明的地方具備共性,那麼不妨可使用文中的幾種技巧來對其進行優化改善,增長代碼的可維護性和可複用性。筆者以前使用TypeScript的機會也很少,因此最近也是一邊學習一邊總結,若是文中有錯誤的地方,還但願可以在評論區指正。
若是你以爲這篇文章的內容對你有幫助,可否幫個忙關注一下筆者的公衆號[前端之境],每週都會努力原創一些前端技術乾貨,關注公衆號後能夠邀你加入前端技術交流羣,咱們能夠一塊兒互相交流,共同進步。
文章已同步更新至Github博客,若覺文章尚可,歡迎前往star!
你的一個點贊,值得讓我付出更多的努力!
逆境中成長,只有不斷地學習,才能成爲更好的本身,與君共勉!