最全的 TypeScript 學習指南

前言

Hello 大家好 我是鯊魚哥 這次給大家帶來的是我曾經非常嫌棄 如今卻 「愛不釋手」「TS 技術」 哈哈 大家看往期文章可能已經發現鯊魚哥之前主要是 Vue 技術棧的 然後因爲 Vue2 和 TS 的結合總感覺不是很絲滑 所以我果斷就在技術選型的時候去掉了 TS(其實我是覺得用起來很煩 和我之前最討厭的 eslint 一樣 各種 「報錯」 讓人不爽)但是 鯊魚哥今年換了新公司 開啓了全新的 「react hook+ts」 這一套組合拳 然後在重新認真學習並在項目裏用上了 ts 之後 確實 「真香」 哈哈 最直觀的感受就是可以幫我們規避很多類型錯誤 更友好的提示 甚至有些方法我們根據定義的類型大概就知道作用是什麼了(去掉了寫註釋的麻煩)況且如今大火的 「Vue3」 也是 TS 重構的 然後 react 和 ts 的結合就更不必說了 所以還沒有開始 ts 的同學就從現在開始跟着鯊魚哥一起來學習吧 最後歡迎大家點擊 鏈接 加入到鯊魚哥的前端羣 內推 討論技術 摸魚 求助 皆可

1 ts 安裝和編譯

cnpm i typescript -g //全局安裝ts
cnpm i -g ts-node //全局安裝ts-node
tsc --init

我們就先按照自動生成的 tsconfig 配置項去使用 裏面的配置咱們可以先不去管他 後續熟練了再去配置

const a: string = "hello";
console.log(a);
tsc index.ts

神奇的事情發生了 項目下出現了一個同名的 index.js 文件 至此我們已經可以把 ts 文件編譯成 js 文件了

不過到這裏聰明的小夥伴就會發現了 我們全局安裝的 「ts-node」 有什麼作用呢 其實這個包是幫助我們在不需要編譯成 js 的前提下就可以直接執行 ts 代碼 比如 我們在控制檯輸入

ts-node index.ts

可以看到我們打印的hello已經輸出了

那可能 還有的小夥伴會發現 我們每次改動都要手動去執行編譯 這樣很麻煩 其實我們可以加一個參數來實現每次文件變動 ts 幫我們 「自動編譯成 js」 的效果

tsc --watch index.ts

好了 環境安裝完畢了 接下來出發去學習 ts 核心吧

2 TS 類型

2.1 布爾類型 (boolean)
const flag: boolean = true;
2.2 Number 類型
const flag: number = 1;
2.3 String 類型
const flag: string = "hello";
2.4 Enum 類型

使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每週的週一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等

// 普通枚舉 初始值默認爲 0 其餘的成員會會按順序自動增長 可以理解爲數組下標
enum Color {
  RED,
  PINK,
  BLUE,
}

const pink: Color = Color.PINK;
console.log(pink); // 1

// 設置初始值
enum Color {
  RED = 10,
  PINK,
  BLUE,
}
const pink: Color = Color.PINK;
console.log(pink); // 11

// 字符串枚舉 每個都需要聲明
enum Color {
  RED = "紅色",
  PINK = "粉色",
  BLUE = "藍色",
}

const pink: Color = Color.PINK;
console.log(pink); // 粉色

// 常量枚舉 它是使用 const 關鍵字修飾的枚舉,常量枚舉與普通枚舉的區別是,整個枚舉會在編譯階段被刪除 我們可以看下編譯之後的效果

const enum Color {
  RED,
  PINK,
  BLUE,
}

const color: Color[] = [Color.RED, Color.PINK, Color.BLUE];

//編譯之後的js如下:
var color = [0 /* RED */, 1 /* PINK */, 2 /* BLUE */];
// 可以看到我們的枚舉並沒有被編譯成js代碼 只是把color這個數組變量編譯出來了
2.5 數組類型 (array)
const flag1: number[] = [1, 2, 3];
const flag2: Array<number> = [1, 2, 3];
2.6 元組類型 (tuple)

在 TypeScript 的基礎類型中,元組( Tuple )表示一個已知 「數量」「類型」 的數組 其實可以理解爲他是一種特殊的數組

const flag: [string, number] = ["hello", 1];
2.7 Symbol

我們在使用 Symbol 的時候,必須添加 es6 的編譯輔助庫 需要在 tsconfig.json 的 libs 字段加上ES2015Symbol 的值是唯一不變的

const sym1 = Symbol("hello");
const sym2 = Symbol("hello");
console.log(Symbol("hello") === Symbol("hello"));
2.8 任意類型 (any)

任何類型都可以被歸爲 any 類型 這讓 any 類型成爲了類型系統的 頂級類型 (也被稱作 全局超級類型) TypeScript 允許我們對 any 類型的值執行任何操作 而無需事先執行任何形式的檢查

一般使用場景:第三方庫沒有提供類型文件時可以使用 any類型轉換遇到困難或者數據結構太複雜難以定義 不過不要太依賴 any 否則就失去了 ts 的意義了

const flag: any = document.getElementById("root");
2.9 null 和 undefined

undefinednull 兩者有各自的類型分別爲 undefinednull

let u: undefined = undefined;
let n: null = null;
2.10 Unknown 類型

unknownany 的主要區別是 unknown 類型會更加嚴格 在對 unknown 類型的值執行大多數操作之前 我們必須進行某種形式的檢查 而在對 any 類型的值執行操作之前 我們不必進行任何檢查 所有類型都可以被歸爲 unknownunknown類型只能被賦值給 any 類型和 unknown 類型本身 而 any 啥都能分配和被分配

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
2.11 void 類型

void 表示沒有任何類型 當一個函數沒有返回值時 TS 會認爲它的返回值是 void 類型。

function hello(name: string): void {}
2.12 never 類型

never 一般表示用戶無法達到的類型 例如never 類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型

function neverReach(): never {
  throw new Error("an error");
}

思考: never 和 void 的區別 void 可以被賦值爲 null 和 undefined 的類型。never 則是一個不包含值的類型。擁有 void 返回值類型的函數能正常運行。擁有 never 返回值類型的函數無法正常返回,無法終止,或會拋出異常。

2.13 BigInt 大數類型

使用 BigInt 可以安全地存儲和操作大整數 我們在使用 BigInt 的時候 必須添加 ESNext 的編譯輔助庫 需要在 tsconfig.json 的 libs 字段加上ESNext要使用1n需要 "target": "ESNext"``numberBigInt 類型不一樣 不兼容

const max1 = Number.MAX_SAFE_INTEGER; // 2**53-1
console.log(max1 + 1 === max1 + 2); //true

const max2 = BigInt(Number.MAX_SAFE_INTEGER);
console.log(max2 + 1n === max2 + 2n); //false

let foo: number;
let bar: bigint;
foo = bar; //error
bar = foo; //error
2.14 object, Object 和 {} 類型

「object」 類型用於表示非原始類型

let objectCase: object;
objectCase = 1; // error
objectCase = "a"; // error
objectCase = true; // error
objectCase = null; // error
objectCase = undefined; // error
objectCase = {}; // ok

「大 Object」 代表所有擁有 toString、hasOwnProperty 方法的類型 所以所有原始類型、非原始類型都可以賦給 Object(嚴格模式下 nullundefined 不可以)

let ObjectCase: Object;
ObjectCase = 1; // ok
ObjectCase = "a"; // ok
ObjectCase = true; // ok
ObjectCase = null; // error
ObjectCase = undefined; // error
ObjectCase = {}; // ok

「{}」 空對象類型和大 Object 一樣 也是表示原始類型和非原始類型的集合

let simpleCase: {};
simpleCase = 1; // ok
simpleCase = "a"; // ok
simpleCase = true; // ok
simpleCase = null; // error
simpleCase = undefined; // error
simpleCase = {}; // ok
2.15 類型推論

指編程語言中能夠自動推導出值的類型的能力 它是一些強靜態類型語言中出現的特性 定義時未賦值就會推論成 any 類型 如果定義的時候就賦值就能利用到類型推論

let flag; //推斷爲any
let count = 123; //爲number類型
let hello = "hello"; //爲string類型
2.16 聯合類型

聯合類型(Union Types)表示取值可以爲多種類型中的一種 未賦值時聯合類型上只能訪問兩個類型共有的屬性和方法

let name: string | number;
console.log(name.toString());
name = 1;
console.log(name.toFixed(2));
name = "hello";
console.log(name.length);
2.17 類型斷言

有時候你會遇到這樣的情況,你會比 TypeScript 更瞭解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。其實就是你需要手動告訴 ts 就按照你斷言的那個類型通過編譯(這一招很關鍵 有時候可以幫助你解決很多編譯報錯)

類型斷言有兩種形式:

// 尖括號 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

以上兩種方式雖然沒有任何區別,但是尖括號格式會與 react 中 JSX 產生語法衝突,因此我們更推薦使用 as 語法。

「非空斷言」 在上下文中當類型檢查器無法斷定類型時 一個新的後綴表達式操作符 ! 可以用於斷言操作對象是非 null 和非 undefined 類型

let flag: null | undefined | string;
flag!.toString(); // ok
flag.toString(); // error
2.18 字面量類型

在 TypeScript 中,字面量不僅可以表示值,還可以表示類型,即所謂的字面量類型。目前,TypeScript 支持 3 種字面量類型:字符串字面量類型、數字字面量類型、布爾字面量類型,對應的字符串字面量、數字字面量、布爾字面量分別擁有與其值一樣的字面量類型,具體示例如下:

let flag1: "hello" = "hello";
let flag2: 1 = 1;
let flag3: true = true;
2.19 類型別名

類型別名用來給一個類型起個新名字

type flag = string | number;

function hello(value: flag) {}
2.20 交叉類型

交叉類型是將多個類型合併爲一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成爲一種類型,它包含了所需的所有類型的特性

type Flag1 = { x: number };
type Flag2 = Flag1 & { y: string };

let flag3: Flag2 = {
  x: 1,
  y: "hello",
  henb,
};
2.21 類型保護

類型保護就是一些表達式,他們在編譯的時候就能通過類型信息確保某個作用域內變量的類型 其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值

「typeof 類型保護」

function double(input: string | number | boolean) {
  if (typeof input === "string") {
    return input + input;
  } else {
    if (typeof input === "number") {
      return input * 2;
    } else {
      return !input;
    }
  }
}

「in 關鍵字」

interface Bird {
  fly: number;
}

interface Dog {
  leg: number;
}

function getNumber(value: Bird | Dog) {
  if ("fly" in value) {
    return value.fly;
  }
  return value.leg;
}

「instanceof 類型保護」

class Animal {
  name!: string;
}
class Bird extends Animal {
  fly!: number;
}
function getName(animal: Animal) {
  if (animal instanceof Bird) {
    console.log(animal.fly);
  } else {
    console.log(animal.name);
  }
}

「自定義類型保護」

通過 type is xxx這樣的類型謂詞來進行類型保護

例如下面的例子 value is object就會認爲如果函數返回 true 那麼定義的 value 就是 object 類型

function isObject(value: unknown): value is object {
  return typeof value === "object" && value !== null;
}

function fn(x: string | object) {
  if (isObject(x)) {
    // ....
  } else {
    // .....
  }
}

3 函數

3.1 函數的定義

可以指定參數的類型和返回值的類型

function hello(name: string): void {
  console.log("hello", name);
}
hello("hahaha");
3.2 函數表達式

定義函數類型

type SumFunc = (x: number, y: number) => number;

let countNumber: SumFunc = function (a, b) {
  return a + b;
};
3.3 可選參數

在 TS 中函數的形參和實參必須一樣,不一樣就要配置可選參數, 而且必須是 「最後一個參數」

function print(name: string, age?: number): void {
  console.log(name, age);
}
print("hahaha");
3.4 默認參數
function ajax(url: string, method: string = "GET") {
  console.log(url, method);
}
ajax("/users");
3.5 剩餘參數
function sum(...numbers: number[]) {
  return numbers.reduce((val, item) =(val += item), 0);
}
console.log(sum(1, 2, 3));
3.6 函數重載

函數重載或方法重載是使用相同名稱和不同參數數量或類型創建多個方法的一種能力。在 TypeScript 中,表現爲給同一個函數提供多個函數類型定義

let obj: any = {};
function attr(val: string): void;
function attr(val: number): void;
function attr(val: any): void {
  if (typeof val === "string") {
    obj.name = val;
  } else {
    obj.age = val;
  }
}
attr("hahaha");
attr(9);
attr(true);
console.log(obj);

注意:函數重載真正執行的是同名函數最後定義的函數體 在最後一個函數體定義之前全都屬於函數類型定義 不能寫具體的函數實現方法 只能定義類型

4 類

4.1 類的定義

在 TypeScript 中,我們可以通過 Class 關鍵字來定義一個類

class Person {
  name!: string; //如果初始屬性沒賦值就需要加上!
  constructor(_name: string) {
    this.name = _name;
  }
  getName(): void {
    console.log(this.name);
  }
}
let p1 = new Person("hello");
p1.getName();

當然 如果我們圖省事 我們也可以把屬性定義直接寫到構造函數的參數裏面去 (不過一般不建議這樣寫 因爲會讓代碼增加閱讀難度)

class Person {
  constructor(public name: string) {}
  getName(): void {
    console.log(this.name);
  }
}
let p1 = new Person("hello");
p1.getName();

注意:當我們定義一個類的時候, 會得到 「2 個類型」 一個是構造函數類型的函數類型 (當做普通構造函數的類型) 另一個是類的實例類型(代表實例)

具體看例子

class Component {
  static myName: string = "靜態名稱屬性";
  myName: string = "實例名稱屬性";
}
//ts 一個類型 一個叫值
//放在=後面的是值
let com = Component; //這裏是代表構造函數
//冒號後面的是類型
let c: Component = new Component(); //這裏是代表實例類型
let f: typeof Component = com;
4.2 存取器

在 TypeScript 中,我們可以通過存取器來改變一個類中屬性的讀取和賦值行爲

class User {
  myname: string;
  constructor(myname: string) {
    this.myname = myname;
  }
  get name() {
    return this.myname;
  }
  set name(value) {
    this.myname = value;
  }
}

let user = new User("hello");
user.name = "world";
console.log(user.name);

其實我們可以看看翻譯成 es5 的代碼 原理很簡單 就是使用了 Object.defineProperty 在類的原型上面攔截了屬性對應的 get 和 set 方法

var User = /** @class */ (function () {
  function User(myname) {
    this.myname = myname;
  }
  Object.defineProperty(User.prototype, "name"{
    get: function () {
      return this.myname;
    },
    set: function (value) {
      this.myname = value;
    },
    enumerable: false,
    configurable: true,
  });
  return User;
})();
var user = new User("hello");
user.name = "world";
console.log(user.name);
4.3 readonly 只讀屬性

readonly 修飾的變量只能在 「構造函數」 中初始化 TypeScript 的類型系統同樣也允許將 interface、type、 class 上的屬性標識爲 readonly readonly 實際上只是在編譯階段進行代碼檢查。

class Animal {
  public readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
  changeName(name: string) {
    this.name = name; //這個ts是報錯的
  }
}

let a = new Animal("hello");
4.4 繼承

子類繼承父類後子類的實例就擁有了父類中的屬性和方法,可以增強代碼的可複用性

將子類公用的方法抽象出來放在父類中,自己的特殊邏輯放在子類中重寫父類的邏輯

super 可以調用父類上的方法和屬性

在 TypeScript 中,我們可以通過 extends 關鍵字來實現繼承

class Person {
  name: string; //定義實例的屬性,默認省略public修飾符
  age: number;
  constructor(name: string, age: number) {
    //構造函數
    this.name = name;
    this.age = age;
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}
class Student extends Person {
  no: number;
  constructor(name: string, age: number, no: number) {
    super(name, age);
    this.no = no;
  }
  getNo(): number {
    return this.no;
  }
}
let s1 = new Student("hello", 10, 1);
console.log(s1);
4.5 類裏面的修飾符

「public」 類裏面 子類 其它任何地方外邊都可以訪問 「protected」 類裏面 子類 都可以訪問, 其它任何地方不能訪問 「private」 類裏面可以訪問,子類和其它任何地方都不可以訪問

class Parent {
  public name: string;
  protected age: number;
  private car: number;
  constructor(name: string, age: number, car: number) {
    //構造函數
    this.name = name;
    this.age = age;
    this.car = car;
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}
class Child extends Parent {
  constructor(name: string, age: number, car: number) {
    super(name, age, car);
  }
  desc() {
    console.log(`${this.name} ${this.age} ${this.car}`); //car訪問不到 會報錯
  }
}

let child = new Child("hello", 10, 1000);
console.log(child.name);
console.log(child.age); //age訪問不到 會報錯
console.log(child.car); //car訪問不到 會報錯
4.6 靜態屬性 靜態方法

類的靜態屬性和方法是直接定義在類本身上面的 所以也只能通過直接調用類的方法和屬性來訪問

class Parent {
  static mainName = "Parent";
  static getmainName() {
    console.log(this); //注意靜態方法裏面的this指向的是類本身 而不是類的實例對象 所以靜態方法裏面只能訪問類的靜態屬性和方法
    return this.mainName;
  }
  public name: string;
  constructor(name: string) {
    //構造函數
    this.name = name;
  }
}
console.log(Parent.mainName);
console.log(Parent.getmainName());
4.7 抽象類和抽象方法

抽象類,無法被實例化,只能被繼承並且無法創建抽象類的實例 子類可以對抽象類進行不同的實現

抽象方法只能出現在抽象類中並且抽象方法不能在抽象類中被具體實現,只能在抽象類的子類中實現(必須要實現)

使用場景:我們一般用抽象類和抽象方法抽離出事物的共性 以後所有繼承的子類必須按照規範去實現自己的具體邏輯 這樣可以增加代碼的可維護性和複用性

使用 abstract 關鍵字來定義抽象類和抽象方法

abstract class Animal {
  name!: string;
  abstract speak(): void;
}
class Cat extends Animal {
  speak() {
    console.log("喵喵喵");
  }
}
let animal = new Animal(); //直接報錯 無法創建抽象類的實例
let cat = new Cat();
cat.speak();

思考 1: 重寫 (override) 和重載 (overload) 的區別

「重寫」 是指子類重寫繼承自父類中的方法 「重載」 是指爲同一個函數提供多個類型定義

class Animal {
  speak(word: string): string {
    return "動物:" + word;
  }
}
class Cat extends Animal {
  speak(word: string): string {
    return "貓:" + word;
  }
}
let cat = new Cat();
console.log(cat.speak("hello"));
// 上面是重寫
//--------------------------------------------
// 下面是重載
function double(val: number): number;
function double(val: string): string;
function double(val: any): any {
  if (typeof val == "number") {
    return val * 2;
  }
  return val + val;
}

let r = double(1);
console.log(r);

思考 2: 什麼是**「多態」**

在父類中定義一個方法,在子類中有多個實現,在程序運行的時候,根據不同的對象執行不同的操作,實現運行時的綁定。

abstract class Animal {
  // 聲明抽象的方法,讓子類去實現
  abstract sleep(): void;
}
class Dog extends Animal {
  sleep() {
    console.log("dog sleep");
  }
}
let dog = new Dog();
class Cat extends Animal {
  sleep() {
    console.log("cat sleep");
  }
}
let cat = new Cat();
let animals: Animal[] = [dog, cat];
animals.forEach((i) ={
  i.sleep();
});

5 接口

接口既可以在面向對象編程中表示爲行爲的抽象,也可以用來描述對象的形狀

我們用 interface 關鍵字來定義接口 在接口中可以用分號或者逗號分割每一項,也可以什麼都不加

5.1 對象的形狀
//接口可以用來描述`對象的形狀`
//接口可以用來描述`對象的形狀`
interface Speakable {
  speak(): void;
  readonly lng: string; //readonly表示只讀屬性 後續不可以更改
  name?: string; //?表示可選屬性
}

let speakman: Speakable = {
  //   speak() {}, //少屬性會報錯
  name: "hello",
  lng: "en",
  age: 111, //多屬性也會報錯
};
5.2 行爲的抽象

接口可以把一些類中共有的屬性和方法抽象出來, 可以用來約束實現此接口的類

一個類可以實現多個接口,一個接口也可以被多個類實現

我們用 implements關鍵字來代表 實現

//接口可以在面向對象編程中表示爲行爲的抽象
interface Speakable {
  speak(): void;
}
interface Eatable {
  eat(): void;
}
//一個類可以實現多個接口
class Person implements Speakable, Eatable {
  speak() {
    console.log("Person說話");
  }
  //   eat() {} //需要實現的接口包含eat方法 不實現會報錯
}
5.3 定義任意屬性

如果我們在定義接口的時候無法預先知道有哪些屬性的時候, 可以使用 [propName:string]:any,propName 名字是任意的

interface Person {
  id: number;
  name: string;
  [propName: string]: any;
}

let p1 = {
  id: 1,
  name: "hello",
  age: 10,
};

這個接口表示 必須要有 id 和 name 這兩個字段 然後還可以新加其餘的未知字段

5.4 接口的繼承

我們除了類可以繼承 接口也可以繼承 同樣的使用 extends關鍵字

interface Speakable {
  speak(): void;
}
interface SpeakChinese extends Speakable {
  speakChinese(): void;
}
class Person implements SpeakChinese {
  speak() {
    console.log("Person");
  }
  speakChinese() {
    console.log("speakChinese");
  }
}
5.5 函數類型接口

可以用接口來定義函數類型

interface discount {
  (price: number): number;
}
let cost: discount = function (price: number): number {
  return price * 0.8;
};
5.6 構造函數的類型接口

使用特殊的 new() 關鍵字來描述類的構造函數類型

class Animal {
  constructor(public name: string) {}
}
//不加new是修飾函數的,加new是修飾類的
interface WithNameClass {
  new (name: string): Animal;
}
function createAnimal(clazz: WithNameClass, name: string) {
  return new clazz(name);
}
let a = createAnimal(Animal, "hello");
console.log(a.name);

其實這樣的用法一般出現在 當我們需要把一個類作爲參數的時候 我們需要對傳入的類的構造函數類型進行約束 所以需要使用 new 關鍵字代表是類的構造函數類型 用以和普通函數進行區分

思考:接口和類型別名的區別 這個題目是經典的 「ts 面試題」

實際上,在大多數的情況下使用接口類型和類型別名的效果等價,但是在某些特定的場景下這兩者還是存在很大區別。

  1. 基礎數據類型 與接口不同,類型別名還可以用於其他類型,如基本類型(原始值)、聯合類型、元組
// primitive
type Name = string;

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement("div");
type B = typeof div;
  1. 重複定義

接口可以定義多次 會被自動合併爲單個接口 類型別名不可以重複定義

interface Point {
  x: number;
}
interface Point {
  y: number;
}
const point: Point = { x: 1, y: 2 };
  1. 擴展 接口可以擴展類型別名,同理,類型別名也可以擴展接口。但是兩者實現擴展的方式不同

接口的擴展就是繼承,通過 extends 來實現。類型別名的擴展就是交叉類型,通過 & 來實現。

// 接口擴展接口
interface PointX {
  x: number;
}

interface Point extends PointX {
  y: number;
}
// ----
// 類型別名擴展類型別名
type PointX = {
  x: number;
};

type Point = PointX & {
  y: number;
};
// ----
// 接口擴展類型別名
type PointX = {
  x: number;
};
interface Point extends PointX {
  y: number;
}
// ----
// 類型別名擴展接口
interface PointX {
  x: number;
}
type Point = PointX & {
  y: number;
};
  1. 實現 這裏有一個特殊情況 類無法實現定義了聯合類型的類型別名
type PartialPoint = { x: number } | { y: number };

// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint {
  // Error
  x = 1;
  y = 2;
}

6 泛型

泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性

爲了更好的瞭解泛型的作用 我們可以看下面的一個例子

function createArray(length: number, value: any): any[] {
  let result = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray(3, "x"); // ['x''x''x']

上述這段代碼用來生成一個長度爲 length 值爲 value 的數組 但是我們其實可以發現一個問題 不管我們傳入什麼類型的 value 返回值的數組永遠是 any 類型 如果我們想要的效果是 我們預先不知道會傳入什麼類型 但是我們希望不管我們傳入什麼類型 我們的返回的數組的指裏面的類型應該和參數保持一致 那麼這時候 泛型就登場了

使用 「泛型」 改造

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray<string>(3, "x"); // ['x''x''x']

我們可以使用 <> 的寫法 然後再面傳入一個變量 T 用來表示後續函數需要用到的類型 當我們真正去調用函數的時候再傳入 T 的類型就可以解決很多預先無法確定類型相關的問題

6.1 多個類型參數

如果我們需要有多個未知的類型佔位 那麼我們可以定義任何的字母來表示不同的類型參數

function swap<T, U>(tuple: [T, U])[U, T] {
  return [tuple[1], tuple[0]];
}

swap([7, "seven"]); // ['seven', 7]
6.2 泛型約束

在函數內部使用泛型變量的時候,由於事先不知道它是哪種類型,所以不能隨意的操作它的屬性或方法:

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含屬性 length,所以編譯的時候報錯了。

這時,我們可以對泛型進行約束,只允許這個函數傳入那些包含 length 屬性的變量。這就是 「泛型約束」

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

注意:我們在泛型裏面使用extends關鍵字代表的是泛型約束 需要和類的繼承區分開

6.3 泛型接口

定義接口的時候也可以指定泛型

interface Cart<T> {
  list: T[];
}
let cart: Cart<{ name: string; price: number }= {
  list: [{ name: "hello", price: 10 }],
};
console.log(cart.list[0].name, cart.list[0].price);

我們定義了接口傳入的類型 T 之後返回的對象數組裏面 T 就是當時傳入的參數類型

6.4 泛型類
class MyArray<T> {
  private list: T[] = [];
  add(value: T) {
    this.list.push(value);
  }
  getMax(): T {
    let result = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
      if (this.list[i] > result) {
        result = this.list[i];
      }
    }
    return result;
  }
}
let arr = new MyArray();
arr.add(1);
arr.add(2);
arr.add(3);
let ret = arr.getMax();
console.log(ret);

上訴例子我們實現了一個在數組裏面添加數字並且獲取最大值的泛型類

6.5 泛型類型別名
type Cart<T> = { list: T[] } | T[];
let c1: Cart<string> = { list: ["1"] };
let c2: Cart<number> = [1];
6.6 泛型參數的默認類型

我們可以爲泛型中的類型參數指定默認類型。當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推測出時,這個默認類型就會起作用

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

7 實用技巧

7.1 typeof 關鍵詞

typeof 關鍵詞除了做類型保護 還可以從實現推出類型,

//先定義變量,再定義類型
let p1 = {
  name: "hello",
  age: 10,
  gender: "male",
};
type People = typeof p1;
function getName(p: People): string {
  return p.name;
}
getName(p1);

上面的例子就是使用 typeof 獲取一個變量的類型

7.2 keyof 關鍵詞

keyof 可以用來取得一個對象接口的所有 key 值

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//type PersonKey = 'name'|'age'|'gender';
type PersonKey = keyof Person;

function getValueByKey(p: Person, key: PersonKey) {
  return p[key];
}
let val = getValueByKey({ name: "hello", age: 10, gender: "male" }"name");
console.log(val);
7.3 索引訪問操作符

使用 [] 操作符可以進行索引訪問

interface Person {
  name: string;
  age: number;
}

type x = Person["name"]; // x is string
7.4 映射類型 in

在定義的時候用 in 操作符去批量定義類型中的屬性

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//批量把一個接口中的屬性都變成可選的
type PartPerson = {
  [Key in keyof Person]?: Person[Key];
};

let p1: PartPerson = {};
7.5 infer 關鍵字

在條件類型語句中,可以用 infer 聲明一個類型變量並且對它進行使用。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

以上代碼中 infer R 就是聲明一個變量來承載傳入函數簽名的返回值類型,簡單說就是用它取到函數返回值的類型方便之後使用。

7.6 內置工具類型
  1. Exclude<T,U> 從 T 可分配給的類型中排除 U
type Exclude<T, U> = T extends U ? never : T;

type E = Exclude<string | number, string>;
let e: E = 10;
  1. Extract<T,U> 從 T 可分配給的類型中提取 U
type Extract<T, U> = T extends U ? T : never;

type E = Extract<string | number, string>;
let e: E = "1";
  1. NonNullable 從 T 中排除 nullundefined
type NonNullable<T> = T extends null | undefined ? never : T;

type E = NonNullable<string | number | null | undefined>;
let e: E = null;
  1. ReturnTypeinfer 最早出現在此 PR 中,表示在 extends 條件語句中待推斷的類型變量
type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;
function getUserInfo() {
  return { name: "hello", age: 10 };
}

// 通過 ReturnType 將 getUserInfo 的返回值類型賦給了 UserInfo
type UserInfo = ReturnType<typeof getUserInfo>;

const userA: UserInfo = {
  name: "hello",
  age: 10,
};

可見 該工具類型主要是獲取函數類型的返回類型

  1. Parameters 該工具類型主要是獲取函數類型的參數類型
type Parameters<T> = T extends (...args: infer R) => any ? R : any;

type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]
type T2 = Parameters<<T>(arg: T) => T>; // [unknown]
  1. PartialPartial 可以將傳入的屬性由非可選變爲可選
type Partial<T> = { [P in keyof T]?: T[P] };
interface A {
  a1: string;
  a2: number;
  a3: boolean;
}
type aPartial = Partial<A>;
const a: aPartial = {}; // 不會報錯
  1. RequiredRequired 可以將傳入的屬性中的可選項變爲必選項,這裏用了 -? 修飾符來實現。
interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
/**
 * type Required<T> = { [P in keyof T]-?: T[P] };
 */
let p: Required<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
  1. ReadonlyReadonly 通過爲傳入的屬性每一項都加上 readonly 修飾符來實現。
interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
//type Readonly<T> = { readonly [P in keyof T]: T[P] };
let p: Readonly<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
p.age = 11; //error
  1. Pick<T,K> Pick 能夠幫助我們從傳入的屬性中摘取某些返回
interface Todo {
  title: string;
  description: string;
  done: boolean;
}
/**
 * From T pick a set of properties K
 * type Pick<T, K extends keyof T> = { [P in K]: T[P] };
 */
type TodoBase = Pick<Todo, "title" | "done">;

// =
type TodoBase = {
  title: string;
  done: boolean;
};
  1. Record<K,T> 構造一個類型,該類型具有一組屬性 K,每個屬性的類型爲 T。可用於將一個類型的屬性映射爲另一個類型。Record 後面的泛型就是對象鍵和值的類型。

簡單理解:K 對應對應的 key,T 對應對象的 value,返回的就是一個聲明好的對象 但是 K 對應的泛型約束是keyof any 也就意味着只能傳入 string|number|symbol

// type Record<K extends keyof any, T> = {
// [P in K]: T;
// };
type Point = "x" | "y";
type PointList = Record<Point, { value: number }>;
const cars: PointList = {
  x: { value: 10 },
  y: { value: 20 },
};
  1. Omit<K,T> 基於已經聲明的類型進行屬性剔除獲得新類型
// type Omit=Pick<T,Exclude<keyof T,K>>
type User = {
id: string;
name: string;
email: string;
};
type UserWithoutEmail = Omit<User, "email">;// UserWithoutEmail ={id: string;name: string;}
};

8 TypeScript 裝飾器

裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明、方法、屬性或參數上,可以修改類的行爲

常見的裝飾器有類裝飾器、屬性裝飾器、方法裝飾器和參數裝飾器

裝飾器的寫法分爲普通裝飾器和裝飾器工廠

使用 @裝飾器的寫法需要把 tsconfig.json 的 experimentalDecorators 字段設置爲 true

8.1 類裝飾器

類裝飾器在類聲明之前聲明,用來監視、修改或替換類定義

namespace a {
  //當裝飾器作爲修飾類的時候,會把構造器傳遞進去
  function addNameEat(constructor: Function) {
    constructor.prototype.name = "hello";
    constructor.prototype.eat = function () {
      console.log("eat");
    };
  }
  @addNameEat
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}

namespace b {
  //還可以使用裝飾器工廠 這樣可以傳遞額外參數
  function addNameEatFactory(name: string) {
    return function (constructor: Function) {
      constructor.prototype.name = name;
      constructor.prototype.eat = function () {
        console.log("eat");
      };
    };
  }
  @addNameEatFactory("hello")
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}

namespace c {
  //還可以替換類,不過替換的類要與原類結構相同
  function enhancer(constructor: Function) {
    return class {
      name: string = "jiagou";
      eat() {
        console.log("喫飯飯");
      }
    };
  }
  @enhancer
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}
8.2 屬性裝飾器

屬性裝飾器表達式會在運行時當作函數被調用,傳入 2 個參數 第一個參數對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象 第二個參數是屬性的名稱

//修飾實例屬性
function upperCase(target: any, propertyKey: string) {
  let value = target[propertyKey];
  const getter = function () {
    return value;
  };
  // 用來替換的setter
  const setter = function (newVal: string) {
    value = newVal.toUpperCase();
  };
  // 替換屬性,先刪除原先的屬性,再重新定義屬性
  if (delete target[propertyKey]) {
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  }
}

class Person {
  @upperCase
  name!: string;
}
let p: Person = new Person();
p.name = "world";
console.log(p.name);
8.3 方法裝飾器

方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:target: Object - 對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象 propertyKey: string | symbol - 方法名 descriptor: TypePropertyDescript - 屬性描述符

//修飾實例方法
function noEnumerable(
  target: any,
  property: string,
  descriptor: PropertyDescriptor
) {
  console.log("target.getName", target.getName);
  console.log("target.getAge", target.getAge);
  descriptor.enumerable = false;
}
//重寫方法
function toNumber(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  let oldMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    args = args.map((item) => parseFloat(item));
    return oldMethod.apply(this, args);
  };
}
class Person {
  name: string = "hello";
  public static age: number = 10;
  constructor() {}
  @noEnumerable
  getName() {
    console.log(this.name);
  }
  @toNumber
  sum(...args: any[]) {
    return args.reduce((accu: number, item: number) => accu + item, 0);
  }
}
let p: Person = new Person();
for (let attr in p) {
  console.log("attr=", attr);
}
p.getName();
console.log(p.sum("1""2""3"));
8.4 參數裝飾器

參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:

target: Object - 被裝飾的類 propertyKey: string | symbol - 方法名 parameterIndex: number - 方法中參數的索引值

function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
 been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase;
  }
}

以上代碼成功運行後,控制檯會輸出以下結果:"The parameter in position 0 at Greeter has been decorated"

8.5 裝飾器執行順序

有多個參數裝飾器時:從最後一個參數依次向前執行

方法和方法參數中參數裝飾器先執行。方法和屬性裝飾器,誰在前面誰先執行。因爲參數屬於方法一部分,所以參數會一直緊緊挨着方法執行

類裝飾器總是最後執行

function Class1Decorator() {
  return function (target: any) {
    console.log("類1裝飾器");
  };
}
function Class2Decorator() {
  return function (target: any) {
    console.log("類2裝飾器");
  };
}
function MethodDecorator() {
  return function (
    target: any,
    methodName: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("方法裝飾器");
  };
}
function Param1Decorator() {
  return function (target: any, methodName: string, paramIndex: number) {
    console.log("參數1裝飾器");
  };
}
function Param2Decorator() {
  return function (target: any, methodName: string, paramIndex: number) {
    console.log("參數2裝飾器");
  };
}
function PropertyDecorator(name: string) {
  return function (target: any, propertyName: string) {
    console.log(name + "屬性裝飾器");
  };
}

@Class1Decorator()
@Class2Decorator()
class Person {
  @PropertyDecorator("name")
  name: string = "hello";
  @PropertyDecorator("age")
  age: number = 10;
  @MethodDecorator()
  greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) {}
}

/**
name屬性裝飾器
age屬性裝飾器
參數2裝飾器
參數1裝飾器
方法裝飾器
類2裝飾器
類1裝飾器
 */

9 編譯

9.1 tsconfig.json 的作用
9.2 tsconfig.json 重要字段
9.3 compilerOptions 選項
{
  "compilerOptions"{

    /* 基本選項 */
    "target""es5",                       // 指定 ECMAScript 目標版本: 'ES3' (default)'ES5''ES6'/'ES2015''ES2016''ES2017', or 'ESNEXT'
    "module""commonjs",                  // 指定使用模塊: 'commonjs''amd''system''umd' or 'es2015'
    "lib"[],                             // 指定要包含在編譯中的庫文件
    "allowJs": true,                       // 允許編譯 javascript 文件
    "checkJs": true,                       // 報告 javascript 文件中的錯誤
    "jsx""preserve",                     // 指定 jsx 代碼的生成: 'preserve''react-native', or 'react'
    "declaration": true,                   // 生成相應的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相應的 '.map' 文件
    "outFile""./",                       // 將輸出文件合併爲一個文件
    "outDir""./",                        // 指定輸出目錄
    "rootDir""./",                       // 用來控制輸出目錄結構 --outDir.
    "removeComments": true,                // 刪除編譯後的所有的註釋
    "noEmit": true,                        // 不生成輸出文件
    "importHelpers": true,                 // 從 tslib 導入輔助工具函數
    "isolatedModules": true,               // 將每個文件做爲單獨的模塊 (與 'ts.transpileModule' 類似).

    /* 嚴格的類型檢查選項 */
    "strict": true,                        // 啓用所有嚴格類型檢查選項
    "noImplicitAny": true,                 // 在表達式和聲明上有隱含的 any類型時報錯
    "strictNullChecks": true,              // 啓用嚴格的 null 檢查
    "noImplicitThis": true,                // 當 this 表達式值爲 any 類型的時候,生成一個錯誤
    "alwaysStrict": true,                  // 以嚴格模式檢查每個模塊,並在每個文件里加入 'use strict'

    /* 額外的檢查 */
    "noUnusedLocals": true,                // 有未使用的變量時,拋出錯誤
    "noUnusedParameters": true,            // 有未使用的參數時,拋出錯誤
    "noImplicitReturns": true,             // 並不是所有函數里的代碼都有返回值時,拋出錯誤
    "noFallthroughCasesInSwitch": true,    // 報告 switch 語句的 fallthrough 錯誤。(即,不允許 switch 的 case 語句貫穿)

    /* 模塊解析選項 */
    "moduleResolution""node",            // 選擇模塊解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl""./",                       // 用於解析非相對模塊名稱的基目錄
    "paths"{},                           // 模塊名到基於 baseUrl 的路徑映射的列表
    "rootDirs"[],                        // 根文件夾列表,其組合內容表示項目運行時的結構內容
    "typeRoots"[],                       // 包含類型聲明的文件列表
    "types"[],                           // 需要包含的類型聲明文件名列表
    "allowSyntheticDefaultImports": true,  // 允許從沒有設置默認導出的模塊中默認導入。

    /* Source Map Options */
    "sourceRoot""./",                    // 指定調試器應該找到 TypeScript 文件而不是源文件的位置
    "mapRoot""./",                       // 指定調試器應該找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成單個 soucemaps 文件,而不是將 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 將代碼與 sourcemaps 生成到一個文件中,要求同時設置了 --inlineSourceMap 或 --sourceMap 屬性

    /* 其他選項 */
    "experimentalDecorators": true,        // 啓用裝飾器
    "emitDecoratorMetadata"true          // 爲裝飾器提供元數據的支持
  }
}

10 模塊和聲明文件

10.1 全局模塊

在默認情況下,當你開始在一個新的 TypeScript 文件中寫下代碼時,它處於全局命名空間中

使用全局變量空間是危險的,因爲它會與文件內的代碼命名衝突。我們推薦使用下文中將要提到的文件模塊

foo.ts

const foo = 123;

bar.ts

const bar = foo; // allowed
10.2 文件模塊

foo.ts

const foo = 123;
export {};

bar.ts

const bar = foo; // error
10.3 聲明文件

typings\jquery.d.ts

declare const $: (selector: string) ={
  click(): void;
  width(length: number): void;
};
10.4 第三方聲明文件
10.5 查找聲明文件
{
    "name""myLib",
    "version""1.0.0",
    "main""lib/index.js",
    "types""myLib.d.ts",
}

查找過程如下:

  1. 先找 myLib.d.ts

  2. 沒有就再找 index.d.ts

  3. 還沒有再找 lib/index.d.js

  4. 還找不到就認爲沒有類型聲明瞭

小結

能看到此處的小夥伴估計是 「真愛粉」 了 哈哈 其實 ts 沒有大家想象的那麼難 可能剛開始接觸的時候會比較牴觸 或者覺得難用 但是隻要堅持下去 慢慢就會發現 「真香定律」 咱們一開始也沒有必要去追求多麼花哨或者高級的用法 如果在 「沒有辦法」 的情況下就用 「any 大法」 也不是不可以 總之首先要用起來 只有不斷地基於實戰練習最終才能掌握一門技術的 「精髓」 鯊魚哥這篇文檔只是理論知識 大家一定要下去多練習 另外 這篇文檔是基於目前網上多方資源和鯊魚哥自己的思考整理出來的個人覺得 「比較全面」 的 ts 學習指南 也感謝很多的優秀博主之前出品的 ts 文章 比如 阿寶哥 俊劫 Jimmy_kiwi 等等

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GHhJ0avPmbVzGPIkguytJw