從 0 到 1 認識 Typescript

最近這兩年,有不少人都在討論 Typescript,不管是社區仍是各類文章都能看出來,總體來講正面的信息是大於負面的,這篇文章就來整理一下我所瞭解的 Typescript。前端

本文首發於公衆號:【前端日誌】,歡迎關注。

本文主要分爲 3 個部分:vue

  • Typescript 基本概念
  • Typescript 高級用法
  • Typescript 總結

Typescript 基本概念

至於官網的定義,這裏就很少作解釋了,你們能夠去官網查看。Typescript 設計目標ios

我理解的定義:賦予 Javascript 類型的概念,讓代碼能夠在運行前就能發現問題。git

Typescript 都有哪些類型

一、Typescript 基本類型,也就是能夠被直接使用的單一類型。github

  • 數字
  • 字符串
  • 布爾類型
  • null
  • undefined
  • any
  • unknown
  • void
  • object
  • 枚舉
  • never

二、複合類型,包含多個單一類型的類型。typescript

  • 數組類型
  • 元組類型
  • 字面量類型
  • 接口類型

三、若是一個類型不能知足要求怎麼辦?axios

  • 可空類型,默認任何類型均可以被賦值成 null 或 undefined。
  • 聯合類型,不肯定類型是哪一個,但能提供幾種選擇,如:type1 | type2。
  • 交叉類型,必須知足多個類型的組合,如:type1 & type2。

類型都在哪裏使用

在 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 高級用法

Typescript 中的基本用法很是簡單,有 js 基礎的同窗很快就能上手,接下來咱們分析一下 Typescript 中更高級的用法,以完成更精密的類型檢查。

類中的高級用法

在類中的高級用法主要有如下幾點:

  • 繼承
  • 存儲器 get set
  • readonly 修飾符
  • 公有,私有,受保護的修飾符
  • 抽象類 abstract

繼承和存儲器和 ES6 裏的功能是一致的,這裏就很少說了,主要說一下類的修飾符和抽象類。

類中的修飾符是體現面向對象封裝性的主要手段,類中的屬性和方法在被不一樣修飾符修飾以後,就有了不一樣權限的劃分,例如:

  • public 表示在當前類、子類、實例中都能訪問。
  • protected 表示只能在當前類、子類中訪問。
  • private 表示只能在當前類訪問。
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) {}
};

函數中的高級用法

函數中的高級用法主要有如下幾點:

  • 函數重載
  • this 類型

函數重載

函數重載指的是一個函數能夠根據不一樣的入參匹配對應的類型。

例如:案例中的 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);

This 類型

咱們都知道,Javascript 中的 this 只有在運行的時候,纔可以判斷,因此對於 Typescript 來講是很難作靜態判斷的,對此 Typescript 給咱們提供了手動綁定 this 類型,讓咱們可以在明確 this 的狀況下,給到靜態的類型提示。

其實在 Javascript 中的 this,就只有這五種狀況:

  • 對象調用,指向調用的對象
  • 全局函數調用,指向 window 對象
  • call apply 調用,指向綁定的對象
  • dom.addEventListener 調用,指向 dom
  • 箭頭函數中的 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
});

泛型

泛型表示的是一個類型在定義時並不肯定,須要在調用的時候才能肯定的類型,主要包含如下幾個知識點:

  • 泛型函數
  • 泛型類
  • 泛型約束 T extends XXX

咱們試想一下,若是一個函數,把傳入的參數直接輸出,咱們怎麼去給它編寫類型?傳入的參數能夠是任何類型,難道咱們須要把每一個類型都寫一遍?

  • 使用函數重載,得把每一個類型都寫一遍,不適合。
  • 泛型,用一個類型佔位 T 去代替,在使用時指定對應的類型便可。
// 使用泛型
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 這個表達式爲例:

  • 協變,表示 B 的結構體必須包含 A 中的全部結構,即:B 中的屬性能夠比 A 多,但不能少。
  • 逆變,和協變相反,即:B 中的全部屬性都在 A 中能找到,能夠比 A 的少。
  • 雙向協變,即沒有規則,B 中的屬性能夠比 A 多,也能夠比 A 少。

對象中的兼容

對象中的兼容,採用的是協變。

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 類型。

那麼問題來了,咱們怎麼去肯定運行時究竟是什麼類型?

答:類型保護。類型保護是針對於聯合類型,讓咱們可以經過邏輯判斷,肯定最終的類型,是來自聯合類型中的哪一個類型。

判斷聯合類型的方法不少:

  • typeof
  • instanceof
  • in
  • 字面量保護,===!=====!=
  • 自定義類型保護,經過判斷是否有某個屬性等
// 自定義類型保護
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 總結

寫了這麼多,接下來講說我對 Typescript 的一些見解。

Typescript 優勢

一、靜態類型檢查,提前發現問題。

二、類型即文檔,便於理解,協做。

三、類型推導,自動補全,提高開發效率。

四、出錯時,能夠大機率排除類型問題,縮短 bug 解決時間。

實戰中的優勢:

一、發現 es 規範中棄用的方法,如:Date.toGMTString。

二、避免了一些不友好的開發代碼,如:動態給 obj 添加屬性。

三、vue 使用變量,若是沒有在 data 定義,會直接拋出問題。

Typescript 缺點

一、短時間增長開發成本。

二、部分庫尚未寫 types 文件。

三、不是徹底的超集。

實戰中的問題:

一、還有一些坑很差解決,axios 編寫了攔截器以後,typescript 反映不到 response 中去。

在這裏插入圖片描述

參考資料

相關文章
相關標籤/搜索