最近這兩年,有不少人都在討論 Typescript,不管是社區仍是各類文章都能看出來,總體來講正面的信息是大於負面的,這篇文章就來整理一下我所瞭解的 Typescript。前端
本文首發於公衆號:【前端日誌】,歡迎關注。
本文主要分爲 3 個部分:vue
至於官網的定義,這裏就很少作解釋了,你們能夠去官網查看。Typescript 設計目標ios
我理解的定義:賦予 Javascript 類型的概念,讓代碼能夠在運行前就能發現問題。git
一、Typescript 基本類型,也就是能夠被直接使用的單一類型。github
二、複合類型,包含多個單一類型的類型。typescript
三、若是一個類型不能知足要求怎麼辦?axios
在 Typescript 中,類型一般在如下幾種狀況下使用。數組
在變量中使用時,直接在變量後面加上類型便可。app
let a: number; let b: string; let c: null; let d: undefined; let e: boolean; let obj: Ixxx = { a: 1, b: 2, }; let fun: Iyyy = () => {};
在類中使用方式和在變量中相似,只是提供了一些專門爲類設計的靜態屬性、靜態方法、成員屬性、構造函數中的類型等。dom
class Greeter { static name:string = 'Greeter' static log(){console.log(‘log')} greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } let greeter = new Greeter("world");
在接口中使用也比較簡單,能夠理解爲組合多個單一類型。
interface IData { name: string; age: number; func: (s: string) => void; }
在函數中使用類型時,主要用於處理函數參數、函數返回值。
// 函數參數 function a(all: string) {} // 函數返回值 function a(a: string): string {} // 可選參數 function a(a: number, b?: number) {}
Typescript 中的基本用法很是簡單,有 js 基礎的同窗很快就能上手,接下來咱們分析一下 Typescript 中更高級的用法,以完成更精密的類型檢查。
在類中的高級用法主要有如下幾點:
繼承和存儲器和 ES6 裏的功能是一致的,這裏就很少說了,主要說一下類的修飾符和抽象類。
類中的修飾符是體現面向對象封裝性的主要手段,類中的屬性和方法在被不一樣修飾符修飾以後,就有了不一樣權限的劃分,例如:
class Animal { // 公有,私有,受保護的修飾符 protected AnimalName: string; readonly age: number; static type: string; private _age: number; // 屬性存儲器 get age(): number { return this._age; } set age(age: number) { this._age = age; } run() { console.log("run", this.AnimalName, this.age); } constructor(theName: string) { this.AnimalName = theName; } } Animal.type = "2"; // 靜態屬性 const dog = new Animal("dog"); dog.age = 2; // 給 readonly 屬性賦值會報錯 dog.AnimalName; // 實例中訪問 protected 報錯 dog.run; // 正常
在類中的繼承也十分簡單,和 ES6 的語法是同樣的。
class Cat extends Animal { dump() { console.log(this.AnimalName); } } let cat = new Cat("catname"); cat.AnimalName; // 受保護的對象,報錯 cat.run; // 正常 cat.age = 2; // 正常
在面向對象中,有一個比較重要的概念就是抽象類,抽象類用於類的抽象,能夠定義一些類的公共屬性、公共方法,讓繼承的子類去實現,也能夠本身實現。
抽象類有如下兩個特色。
tip 經典問題:抽象類的接口的區別
抽象類要被子類繼承,接口要被類實現。
- 在 ts 中使用 extends 去繼承一個抽象類。
- 在 ts 中使用 implements 去實現一個接口。
- 接口只能作方法聲明,抽象類中能夠做方法聲明,也能夠作方法實現。
- 抽象類是有規律的,抽離的是一個類別的公共部分,而接口只是對相同屬性和方法的抽象,屬性和方法能夠無任何關聯。
抽象類的用法以下。
abstract class Animal { abstract makeSound(): void; // 直接定義方法實例 move(): void { console.log("roaming the earch..."); } } class Cat extends Animal { makeSound() {} // 必須實現的抽象方法 move() { console.log('move'); } } new Cat3();
接口中的高級用法主要有如下幾點:
接口中除了能夠定義常規屬性以外,還能夠定義可選屬性、索引類型等。
interface Ia { a: string; b?: string; // 可選屬性 readonly c: number; // 只讀屬性 [key: number]: string; // 索引類型 } // 接口繼承 interface Ib extends Ia { age: number; } let test1: Ia = { a: "", c: 2, age: 1, }; test1.c = 2; // 報錯,只讀屬性 const item0 = test1[0]; // 索引類型
接口中同時也支持定義函數類型、構造函數類型。
// 接口定義函數類型 interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc = function (x: string, y: string) { return false; }; // 接口中編寫類的構造函數類型檢查 interface IClass { new (hour: number, minute: number); } let test2: IClass = class { constructor(x: number, y: number) {} };
函數中的高級用法主要有如下幾點:
函數重載指的是一個函數能夠根據不一樣的入參匹配對應的類型。
例如:案例中的 doSomeThing
在傳一個參數的時候被提示爲 number
類型,傳兩個參數的話,第一個參數就必須是 string
類型。
// 函數重載 function doSomeThing(x: string, y: number): string; function doSomeThing(x: number): string; function doSomeThing(x): any {} let result = doSomeThing(0); let result1 = doSomeThing("", 2);
咱們都知道,Javascript 中的 this 只有在運行的時候,纔可以判斷,因此對於 Typescript 來講是很難作靜態判斷的,對此 Typescript 給咱們提供了手動綁定 this 類型,讓咱們可以在明確 this 的狀況下,給到靜態的類型提示。
其實在 Javascript 中的 this,就只有這五種狀況:
// 全局函數調用 - window function doSomeThing() { return this; } const result2 = doSomeThing(); // 對象調用 - 對象 interface IObj { age: number; // 手動指定 this 類型 doSomeThing(this: IObj): IObj; doSomeThing2(): Function; } const obj: IObj = { age: 12, doSomeThing: function () { return this; }, doSomeThing2: () => { console.log(this); }, }; const result3 = obj.doSomeThing(); let globalDoSomeThing = obj.doSomeThing; globalDoSomeThing(); // 這樣會報錯,由於咱們只容許在對象中調用 // call apply 綁定對應的對象 function fn() { console.log(this); } fn.bind(document)(); // dom.addEventListener document.body.addEventListener("click", function () { console.log(this); // body });
泛型表示的是一個類型在定義時並不肯定,須要在調用的時候才能肯定的類型,主要包含如下幾個知識點:
咱們試想一下,若是一個函數,把傳入的參數直接輸出,咱們怎麼去給它編寫類型?傳入的參數能夠是任何類型,難道咱們須要把每一個類型都寫一遍?
// 使用泛型 function doSomeThing<T>(param: T): T { return param; } let y = doSomeThing(1); // 泛型類 class MyClass<T> { log(msg: T) { return msg; } } let my = new MyClass<string>(); my.log(""); // 泛型約束,能夠規定最終執行時,只能是哪些類型 function d2<T extends string | number>(param: T): T { return param; } let z = d2(true);
其實泛型原本很簡單,但許多初學 Typescript 的同窗以爲泛型很難,實際上是由於泛型能夠結合索引查詢符 keyof
、索引訪問符 T[k]
等寫出難以閱讀的代碼,咱們來看一下。
// 如下四種方法,表達的含義是一致的,都是把對象中的某一個屬性的 value 取出來,組成一個數組 function showKey1<K extends keyof T, T>(items: K[], obj: T): T[K][] { return items.map((item) => obj[item]); } function showKey2<K extends keyof T, T>(items: K[], obj: T): Array<T[K]> { return items.map((item) => obj[item]); } function showKey3<K extends keyof T, T>( items: K[], obj: { [K in keyof T]: any } ): T[K][] { return items.map((item) => obj[item]); } function showKey4<K extends keyof T, T>( items: K[], obj: { [K in keyof T]: any } ): Array<T[K]> { return items.map((item) => obj[item]); } let obj22 = showKey4<"age", { name: string; age: number }>(["age"], { name: "yhl", age: 12, });
類型兼容性是我認爲 Typescript 中最難理解的一個部分,咱們來分析一下。
在 Typescript 中是經過結構體來判斷兼容性的,若是兩個的結構體一致,就直接兼容了,但若是不一致,Typescript 給咱們提供了一下兩種兼容方式:
以 A = B
這個表達式爲例:
對象中的兼容,採用的是協變。
let obj1 = { a: 1, b: "b", c: true, }; let obj2 = { a: 1, }; obj2 = obj1; obj1 = obj2; // 報錯,由於 obj2 屬性不夠
函數返回值中的兼容,採用的是協變。
let fun1 = function (): { a: number; b: string } { return { a: 1, b: "" }; }; let fun2 = function (): { a: number } { return { a: 1 }; }; fun1 = fun2; // 報錯,fun2 中沒有 b 參數 fun2 = fun1;
函數參數個數的兼容,採用的是逆變。
// 若是函數中的全部參數,均可以在賦值目標中找到,就能賦值 let fun1 = function (a: number, b: string) {}; let fun2 = function (a: number) {}; fun1 = fun2; fun2 = fun1; // 報錯, fun1 中的 b 參數不能再 fun2 中找到
函數參數兼容,採用的是雙向協變。
let fn1 = (a: { name: string; age: number }) => { console.log("使用 name 和 age"); }; let fn2 = (a: { name: string }) => { console.log("使用 name"); }; fn2 = fn1; // 正常 fn1 = fn2; // 正常
tip 理解函數參數雙向協變
一、咱們思考一下,一個函數
dog => dog
,它的子函數是什麼?
注意:原函數若是被修改爲了另外一個函數,但他的類型是不會改變的,ts 仍是會按照原函數的類型去作類型檢查!
grayDog => grayDog
- 不對,若是傳了其餘類型的 dog,沒有 grayDog 的方法,會報錯。
grayDog => animal
- 同上。
animal => animal
- 返回值不對,返回值始終是協變的,必須多傳。
animal => grayDog
- 正確。
因此,函數參數類型應該是逆變的。
二、爲何 Typescript 中的函數參數也是協變呢?
enum EventType { Mouse, Keyboard } interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ } listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));
上面代碼中,咱們在調用時傳的是 mouse 類型,因此在回調函數中,咱們是知道返回的參數必定是一個 MouseEvent 類型,這樣是符合邏輯的,但因爲 MouseEvent 類型的屬性是多於 Event 類型的,因此說 Typescript 的參數類型也是支持協變的。
:::
類中的兼容,是在比較兩個實例中的結構體,是一種協變。
class Student1 { name: string; // private weight:number } class Student2 { // extends Student1 name: string; age: number; } let student1 = new Student1(); let student2 = new Student2(); student1 = student2; student2 = student1; // 報錯,student1 沒有 age 參數
須要注意的是,實例中的屬性和方法會受到類中修飾符的影響,若是是 private 修飾符,那麼必須保證二者之間的 private 修飾的屬性來自同一對象。如上文中若是把 private 註釋放開的話,只能經過繼承去實現兼容。
泛型中的兼容,若是沒有用到 T,則兩個泛型也是兼容的。
interface Empty<T> {} let x1: Empty<number>; let y1: Empty<string>; x1 = y1; y1 = x1;
Typescript 中的高級類型包括:交叉類型、聯合類型、字面量類型、索引類型、映射類型等,這裏咱們主要討論一下
聯合類型是指一個對象多是多個類型中的一個,如:let a :number | string
表示 a 要麼是 number 類型,要麼是 string 類型。
那麼問題來了,咱們怎麼去肯定運行時究竟是什麼類型?
答:類型保護。類型保護是針對於聯合類型,讓咱們可以經過邏輯判斷,肯定最終的類型,是來自聯合類型中的哪一個類型。
判斷聯合類型的方法不少:
===
、!===
、==
、!=
// 自定義類型保護 function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; } if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
映射類型表示能夠對某一個類型進行操做,產生出另外一個符合咱們要求的類型:
ReadOnly<T>
,將 T 中的類型都變爲只讀。Partial<T>
,將 T 中的類型都變爲可選。Exclude<T, U>
,從 T 中剔除能夠賦值給 U 的類型。Extract<T, U>
,提取 T 中能夠賦值給 U 的類型。NonNullable<T>
,從 T 中剔除 null 和 undefined。ReturnType<T>
,獲取函數返回值類型。InstanceType<T>
,獲取構造函數類型的實例類型。咱們也能夠編寫自定義的映射類型。
//定義toPromise映射 type ToPromise<T> = { [K in keyof T]: Promise<T[K]> }; type NumberList = [number, number]; type PromiseCoordinate = ToPromise<NumberList>; // [Promise<number>, Promise<number>]
寫了這麼多,接下來講說我對 Typescript 的一些見解。
一、靜態類型檢查,提前發現問題。
二、類型即文檔,便於理解,協做。
三、類型推導,自動補全,提高開發效率。
四、出錯時,能夠大機率排除類型問題,縮短 bug 解決時間。
實戰中的優勢:
一、發現 es 規範中棄用的方法,如:Date.toGMTString。
二、避免了一些不友好的開發代碼,如:動態給 obj 添加屬性。
三、vue 使用變量,若是沒有在 data 定義,會直接拋出問題。
一、短時間增長開發成本。
二、部分庫尚未寫 types 文件。
三、不是徹底的超集。
實戰中的問題:
一、還有一些坑很差解決,axios 編寫了攔截器以後,typescript 反映不到 response 中去。