最近這兩年,有很多人都在討論 Typescript,無論是社區還是各種文章都能看出來,整體來說正面的信息是大於負面的,這篇文章就來整理一下我所了解的 Typescript。
Typescript 類型
Typescript有哪些類型
1、Typescript 基本類型,也就是可以被直接使用的單一類型。
- 數字
- 字符串
- 布爾類型
- null
- undefined
- any
- unknown
- void
- object
- 枚舉
- never
2、復合類型,包含多個單一類型的類型。
- 數組類型
- 元組類型
- 字面量類型
- 接口類型
3、如果一個類型不能滿足要求怎么辦?
- 可空類型,默認任何類型都可以被賦值成 null 或 undefined。
- 聯合類型,不確定類型是哪個,但能提供幾種選擇,如:type1 | type2。
- 交叉類型,必須滿足多個類型的組合,如:
type1 & type2。
類型都在哪里使用
在變量中使用
在變量中使用時,直接在變量后面加上類型即可。
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 = () => {};
在接口中使用
在接口中使用也比較簡單,可以理解為組合多個單一類型。
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 修飾符
- df公有,私有,受保護的修飾符
- 抽象類 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) {}
};
函數中的高級用法
函數重載
函數重載指的是一個函數可以根據不同的入參匹配對應的類型。
例如:案例中的 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 中的高級類型包括:交叉類型、聯合類型、字面量類型、索引類型、映射類型等,這里我們主要討論一下
- 聯合類型
- 映射類型
聯合類型
聯合類型是指一個對象可能是多個類型中的一個,如: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 中的類型都變為只讀。
- Partial,將 T 中的類型都變為可選。
- Exclude,從 T 中剔除可以賦值給 U 的類型。
- Extract,提取 T 中可以賦值給 U 的類型。
- NonNullable,從 T 中剔除 null 和 undefined。
- ReturnType,獲取函數返回值類型。
- InstanceType,獲取構造函數類型的實例類型。
我們也可以編寫自定義的映射類型。
//定義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 優點
1、靜態類型檢查,提早發現問題。
2、類型即文檔,便於理解,協作。
3、類型推導,自動補全,提升開發效率。
4、出錯時,可以大概率排除類型問題,縮短 bug 解決時間。
實戰中的優點:
1、發現 es 規范中棄用的方法,如:Date.toGMTString。
2、避免了一些不友好的開發代碼,如:動態給 obj 添加屬性。
3、vue 使用變量,如果沒有在 data 定義,會直接拋出問題。
Typescript 缺點
1、短期增加開發成本。
2、部分庫還沒有寫 types 文件。
3、不是完全的超集。
實戰中的問題:
1、還有一些坑不好解決,axios 編寫了攔截器之后,typescript 反映不到 response 中去。
Typescript學習
對於Typescript的入門學習,我自己學習時看的是阿里大佬寫的Typescript學習指南,一共分為16個類目去講解Typescript ,包括文章中講到的內容也在這份指南中,對於想ts詳細學習的或想查漏補缺的小伙伴都很適合。
篇幅原因就不列舉Typescript學習指南文檔內容,完整版的【直接點擊獲取】,一起進入TS的世界。


結尾
假如你工作在一個大中型項目上面,typescript 對你應該是利大於弊。可以學!還能從另外一個方便了解靜態類型語言是怎么玩的,看到別人的 Java 代碼居然能有看得懂的部分了。 當然要學會根據自己的需求和項目的規模合理選用工具,如果你的應用就是一個簡單的展示頁面,加幾個 UI 狀態改變,就沒有必要使用。
