Typescript 使用日志(干貨)
Typescript 使用日志
最近這兩年,有很多人都在討論 Typescript,無論是社區(qū)還是各種文章都能看出來,整體來說正面的信息是大于負(fù)面的,這篇文章就來整理一下我所了解的 Typescript。
本文主要分為 3 個(gè)部分:
?Typescript 基本概念?Typescript 高級(jí)用法?Typescript 總結(jié)
Typescript 基本概念
至于官網(wǎng)的定義,這里就不多做解釋了,大家可以去官網(wǎng)查看。Typescript 設(shè)計(jì)目標(biāo)[1]
我理解的定義:賦予 Javascript 類型的概念,讓代碼可以在運(yùn)行前就能發(fā)現(xiàn)問題。
Typescript 都有哪些類型
1、Typescript 基本類型,也就是可以被直接使用的單一類型。
?數(shù)字?字符串?布爾類型?null?undefined?any?unknown?void?object?枚舉?never
2、復(fù)合類型,包含多個(gè)單一類型的類型。
?數(shù)組類型?元組類型?字面量類型?接口類型
3、如果一個(gè)類型不能滿足要求怎么辦?
?可空類型,默認(rèn)任何類型都可以被賦值成 null 或 undefined。?聯(lián)合類型,不確定類型是哪個(gè),但能提供幾種選擇,如:type1 | type2。?交叉類型,必須滿足多個(gè)類型的組合,如:type1 & type2。
類型都在哪里使用
在 Typescript 中,類型通常在以下幾種情況下使用。
?變量中使用?類中使用?接口中使用?函數(shù)中使用
類型在變量中使用
在變量中使用時(shí),直接在變量后面加上類型即可。
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 = () => {};
類型在類中使用
在類中使用方式和在變量中類似,只是提供了一些專門為類設(shè)計(jì)的靜態(tài)屬性、靜態(tài)方法、成員屬性、構(gòu)造函數(shù)中的類型等。
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");
類型在接口中使用
在接口中使用也比較簡(jiǎn)單,可以理解為組合多個(gè)單一類型。
interface IData {name: string;age: number;func: (s: string) => void;}
類型在函數(shù)中使用
在函數(shù)中使用類型時(shí),主要用于處理函數(shù)參數(shù)、函數(shù)返回值。
// 函數(shù)參數(shù)function a(all: string) {}// 函數(shù)返回值function a(a: string): string {}// 可選參數(shù)function a(a: number, b?: number) {}
Typescript 高級(jí)用法
Typescript 中的基本用法非常簡(jiǎn)單,有 js 基礎(chǔ)的同學(xué)很快就能上手,接下來我們分析一下 Typescript 中更高級(jí)的用法,以完成更精密的類型檢查。
類中的高級(jí)用法
在類中的高級(jí)用法主要有以下幾點(diǎn):
?繼承?存儲(chǔ)器 get set?readonly 修飾符?公有,私有,受保護(hù)的修飾符?抽象類 abstract
繼承和存儲(chǔ)器和 ES6 里的功能是一致的,這里就不多說了,主要說一下類的修飾符和抽象類。
類中的修飾符是體現(xiàn)面向?qū)ο蠓庋b性的主要手段,類中的屬性和方法在被不同修飾符修飾之后,就有了不同權(quán)限的劃分,例如:
?public 表示在當(dāng)前類、子類、實(shí)例中都能訪問。?protected 表示只能在當(dāng)前類、子類中訪問。?private 表示只能在當(dāng)前類訪問。
class Animal {// 公有,私有,受保護(hù)的修飾符protected AnimalName: string;readonly age: number;static type: string;private _age: number;// 屬性存儲(chǔ)器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"; // 靜態(tài)屬性const dog = new Animal("dog");dog.age = 2; // 給 readonly 屬性賦值會(huì)報(bào)錯(cuò)dog.AnimalName; // 實(shí)例中訪問 protected 報(bào)錯(cuò)dog.run; // 正常
在類中的繼承也十分簡(jiǎn)單,和 ES6 的語法是一樣的。
class Cat extends Animal {dump() {console.log(this.AnimalName);}}let cat = new Cat("catname");cat.AnimalName; // 受保護(hù)的對(duì)象,報(bào)錯(cuò)cat.run; // 正常cat.age = 2; // 正常
在面向?qū)ο笾校幸粋€(gè)比較重要的概念就是抽象類,抽象類用于類的抽象,可以定義一些類的公共屬性、公共方法,讓繼承的子類去實(shí)現(xiàn),也可以自己實(shí)現(xiàn)。
抽象類有以下兩個(gè)特點(diǎn)。
?抽象類不能直接實(shí)例化?抽象類中的抽象屬性和方法,必須被子類實(shí)現(xiàn)
tip 經(jīng)典問題:抽象類的接口的區(qū)別
?抽象類要被子類繼承,接口要被類實(shí)現(xiàn)。?在 ts 中使用 extends 去繼承一個(gè)抽象類。?在 ts 中使用 implements 去實(shí)現(xiàn)一個(gè)接口。?接口只能做方法聲明,抽象類中可以作方法聲明,也可以做方法實(shí)現(xiàn)。?抽象類是有規(guī)律的,抽離的是一個(gè)類別的公共部分,而接口只是對(duì)相同屬性和方法的抽象,屬性和方法可以無任何關(guān)聯(lián)。
抽象類的用法如下。
abstract class Animal {abstract makeSound(): void;// 直接定義方法實(shí)例move(): void {console.log("roaming the earch...");}}class Cat extends Animal {makeSound() {} // 必須實(shí)現(xiàn)的抽象方法move() {console.log('move');}}new Cat3();
接口中的高級(jí)用法
接口中的高級(jí)用法主要有以下幾點(diǎn):
?繼承?可選屬性?只讀屬性?索引類型:字符串和數(shù)字?函數(shù)類型接口?給類添加類型,構(gòu)造函數(shù)類型
接口中除了可以定義常規(guī)屬性之外,還可以定義可選屬性、索引類型等。
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; // 報(bào)錯(cuò),只讀屬性const item0 = test1[0]; // 索引類型
接口中同時(shí)也支持定義函數(shù)類型、構(gòu)造函數(shù)類型。
// 接口定義函數(shù)類型interface SearchFunc {(source: string, subString: string): boolean;}let mySearch: SearchFunc = function (x: string, y: string) {return false;};// 接口中編寫類的構(gòu)造函數(shù)類型檢查interface IClass {new (hour: number, minute: number);}let test2: IClass = class {constructor(x: number, y: number) {}};
函數(shù)中的高級(jí)用法
函數(shù)中的高級(jí)用法主要有以下幾點(diǎn):
?函數(shù)重載?this 類型
函數(shù)重載
函數(shù)重載指的是一個(gè)函數(shù)可以根據(jù)不同的入?yún)⑵ヅ鋵?duì)應(yīng)的類型。
例如:案例中的?doSomeThing?在傳一個(gè)參數(shù)的時(shí)候被提示為?number?類型,傳兩個(gè)參數(shù)的話,第一個(gè)參數(shù)就必須是?string?類型。
// 函數(shù)重載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 只有在運(yùn)行的時(shí)候,才能夠判斷,所以對(duì)于 Typescript 來說是很難做靜態(tài)判斷的,對(duì)此 Typescript 給我們提供了手動(dòng)綁定 this 類型,讓我們能夠在明確 this 的情況下,給到靜態(tài)的類型提示。
其實(shí)在 Javascript 中的 this,就只有這五種情況:
?對(duì)象調(diào)用,指向調(diào)用的對(duì)象?全局函數(shù)調(diào)用,指向 window 對(duì)象?call apply 調(diào)用,指向綁定的對(duì)象?dom.addEventListener 調(diào)用,指向 dom?箭頭函數(shù)中的 this ,指向綁定時(shí)的上下文
// 全局函數(shù)調(diào)用 - windowfunction doSomeThing() {return this;}const result2 = doSomeThing();// 對(duì)象調(diào)用 - 對(duì)象interface IObj {age: number;// 手動(dòng)指定 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(); // 這樣會(huì)報(bào)錯(cuò),因?yàn)槲覀冎辉试S在對(duì)象中調(diào)用// call apply 綁定對(duì)應(yīng)的對(duì)象function fn() {console.log(this);}fn.bind(document)();// dom.addEventListenerdocument.body.addEventListener("click", function () {console.log(this); // body});
泛型
泛型表示的是一個(gè)類型在定義時(shí)并不確定,需要在調(diào)用的時(shí)候才能確定的類型,主要包含以下幾個(gè)知識(shí)點(diǎn):
?泛型函數(shù)?泛型類?泛型約束 T extends XXX
我們?cè)囅胍幌拢绻粋€(gè)函數(shù),把傳入的參數(shù)直接輸出,我們?cè)趺慈ソo它編寫類型?傳入的參數(shù)可以是任何類型,難道我們需要把每個(gè)類型都寫一遍?
?使用函數(shù)重載,得把每個(gè)類型都寫一遍,不適合。?泛型,用一個(gè)類型占位 T 去代替,在使用時(shí)指定對(duì)應(yīng)的類型即可。
// 使用泛型function doSomeThing(param: T): T { return param;}let y = doSomeThing(1);// 泛型類class MyClass{ log(msg: T) {return msg;}}let my = new MyClass(); my.log("");// 泛型約束,可以規(guī)定最終執(zhí)行時(shí),只能是哪些類型function d2(param: T): T { return param;}let z = d2(true);
其實(shí)泛型本來很簡(jiǎn)單,但許多初學(xué) Typescript 的同學(xué)覺得泛型很難,其實(shí)是因?yàn)榉盒涂梢越Y(jié)合索引查詢符?keyof、索引訪問符?T[k]?等寫出難以閱讀的代碼,我們來看一下。
// 以下四種方法,表達(dá)的含義是一致的,都是把對(duì)象中的某一個(gè)屬性的 value 取出來,組成一個(gè)數(shù)組function showKey1(items: K[], obj: T): T[K][] { return items.map((item) => obj[item]);}function showKey2(items: K[], obj: T): Array { return items.map((item) => obj[item]);}function showKey3( items: K[],obj: { [K in keyof T]: any }): T[K][] {return items.map((item) => obj[item]);}function showKey4( items: K[],obj: { [K in keyof T]: any }): Array{ return items.map((item) => obj[item]);}let obj22 = showKey4<"age", { name: string; age: number }>(["age"], {name: "yhl",age: 12,});
類型兼容性
類型兼容性是我認(rèn)為 Typescript 中最難理解的一個(gè)部分,我們來分析一下。
?對(duì)象中的兼容?函數(shù)返回值兼容?函數(shù)參數(shù)列表兼容?函數(shù)參數(shù)結(jié)構(gòu)兼容?類中的兼容?泛型中的兼容
在 Typescript 中是通過結(jié)構(gòu)體來判斷兼容性的,如果兩個(gè)的結(jié)構(gòu)體一致,就直接兼容了,但如果不一致,Typescript 給我們提供了一下兩種兼容方式:
以?A = B?這個(gè)表達(dá)式為例:
?協(xié)變,表示 B 的結(jié)構(gòu)體必須包含 A 中的所有結(jié)構(gòu),即:B 中的屬性可以比 A 多,但不能少。?逆變,和協(xié)變相反,即:B 中的所有屬性都在 A 中能找到,可以比 A 的少。?雙向協(xié)變,即沒有規(guī)則,B 中的屬性可以比 A 多,也可以比 A 少。
對(duì)象中的兼容
對(duì)象中的兼容,采用的是協(xié)變。
let obj1 = {a: 1,b: "b",c: true,};let obj2 = {a: 1,};obj2 = obj1;obj1 = obj2; // 報(bào)錯(cuò),因?yàn)?obj2 屬性不夠
函數(shù)返回值兼容
函數(shù)返回值中的兼容,采用的是協(xié)變。
let fun1 = function (): { a: number; b: string } {return { a: 1, b: "" };};let fun2 = function (): { a: number } {return { a: 1 };};fun1 = fun2; // 報(bào)錯(cuò),fun2 中沒有 b 參數(shù)fun2 = fun1;
函數(shù)參數(shù)個(gè)數(shù)兼容
函數(shù)參數(shù)個(gè)數(shù)的兼容,采用的是逆變。
// 如果函數(shù)中的所有參數(shù),都可以在賦值目標(biāo)中找到,就能賦值let fun1 = function (a: number, b: string) {};let fun2 = function (a: number) {};fun1 = fun2;fun2 = fun1; // 報(bào)錯(cuò), fun1 中的 b 參數(shù)不能再 fun2 中找到
函數(shù)參數(shù)兼容
函數(shù)參數(shù)兼容,采用的是雙向協(xié)變。
let fn1 = (a: { name: string; age: number }) => {console.log("使用 name 和 age");};let fn2 = (a: { name: string }) => {console.log("使用 name");};fn2 = fn1; // 正常fn1 = fn2; // 正常
理解函數(shù)參數(shù)雙向協(xié)變
1、我們思考一下,一個(gè)函數(shù)?dog => dog,它的子函數(shù)是什么?
注意:原函數(shù)如果被修改成了另一個(gè)函數(shù),但他的類型是不會(huì)改變的,ts 還是會(huì)按照原函數(shù)的類型去做類型檢查!
?grayDog => grayDog
? ? ?不對(duì),如果傳了其他類型的 dog,沒有 grayDog 的方法,會(huì)報(bào)錯(cuò)。
? ? ?grayDog => animal
? ?同上。
? ? ?animal => animal
? ?返回值不對(duì),返回值始終是協(xié)變的,必須多傳。
? ? ?animal => grayDog
? ?正確。
所以,函數(shù)參數(shù)類型應(yīng)該是逆變的。
2、為什么 Typescript 中的函數(shù)參數(shù)也是協(xié)變呢?
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));
上面代碼中,我們?cè)谡{(diào)用時(shí)傳的是 mouse 類型,所以在回調(diào)函數(shù)中,我們是知道返回的參數(shù)一定是一個(gè) MouseEvent 類型,這樣是符合邏輯的,但由于 MouseEvent 類型的屬性是多于 Event 類型的,所以說 Typescript 的參數(shù)類型也是支持協(xié)變的。
類中的兼容
類中的兼容,是在比較兩個(gè)實(shí)例中的結(jié)構(gòu)體,是一種協(xié)變。
class Student1 {name: string;// private weight:number}class Student2 {// extends Student1name: string;age: number;}let student1 = new Student1();let student2 = new Student2();student1 = student2;student2 = student1; // 報(bào)錯(cuò),student1 沒有 age 參數(shù)
需要注意的是,實(shí)例中的屬性和方法會(huì)受到類中修飾符的影響,如果是 private 修飾符,那么必須保證兩者之間的 private 修飾的屬性來自同一對(duì)象。如上文中如果把 private 注釋放開的話,只能通過繼承去實(shí)現(xiàn)兼容。
泛型中的兼容
泛型中的兼容,如果沒有用到 T,則兩個(gè)泛型也是兼容的。
interface Empty{} let x1: Empty; let y1: Empty; x1 = y1;y1 = x1;
高級(jí)類型
Typescript 中的高級(jí)類型包括:交叉類型、聯(lián)合類型、字面量類型、索引類型、映射類型等,這里我們主要討論一下
?聯(lián)合類型?映射類型
聯(lián)合類型
聯(lián)合類型是指一個(gè)對(duì)象可能是多個(gè)類型中的一個(gè),如:let a :number | string?表示 a 要么是 number 類型,要么是 string 類型。
那么問題來了,我們?cè)趺慈ゴ_定運(yùn)行時(shí)到底是什么類型?
答:類型保護(hù)。類型保護(hù)是針對(duì)于聯(lián)合類型,讓我們能夠通過邏輯判斷,確定最終的類型,是來自聯(lián)合類型中的哪個(gè)類型。
判斷聯(lián)合類型的方法很多:
?typeof?instanceof?in?字面量保護(hù),===、!===、==、!=?自定義類型保護(hù),通過判斷是否有某個(gè)屬性等
// 自定義類型保護(hù)function isFish(pet: Fish | Bird): pet is Fish {return (pet).swim !== undefined; }if (isFish(pet)) {pet.swim();} else {pet.fly();}
映射類型
映射類型表示可以對(duì)某一個(gè)類型進(jìn)行操作,產(chǎn)生出另一個(gè)符合我們要求的類型:
?ReadOnly,將 T 中的類型都變?yōu)橹蛔x。?Partial,將 T 中的類型都變?yōu)榭蛇x。?Exclude,從 T 中剔除可以賦值給 U 的類型。?Extract,提取 T 中可以賦值給 U 的類型。?NonNullable,從 T 中剔除 null 和 undefined。?ReturnType,獲取函數(shù)返回值類型。?InstanceType,獲取構(gòu)造函數(shù)類型的實(shí)例類型。
我們也可以編寫自定義的映射類型。
//定義toPromise映射type ToPromise= { [K in keyof T]: Promise }; type NumberList = [number, number];type PromiseCoordinate = ToPromise; // [Promise, Promise ]
Typescript 總結(jié)
寫了這么多,接下來說說我對(duì) Typescript 的一些看法。
Typescript 優(yōu)點(diǎn)
1、靜態(tài)類型檢查,提早發(fā)現(xiàn)問題。
2、類型即文檔,便于理解,協(xié)作。
3、類型推導(dǎo),自動(dòng)補(bǔ)全,提升開發(fā)效率。
4、出錯(cuò)時(shí),可以大概率排除類型問題,縮短 bug 解決時(shí)間。
實(shí)戰(zhàn)中的優(yōu)點(diǎn):
1、發(fā)現(xiàn) es 規(guī)范中棄用的方法,如:Date.toGMTString。
2、避免了一些不友好的開發(fā)代碼,如:動(dòng)態(tài)給 obj 添加屬性。
3、vue 使用變量,如果沒有在 data 定義,會(huì)直接拋出問題。
Typescript 缺點(diǎn)
1、短期增加開發(fā)成本。
2、部分庫還沒有寫 types 文件。
3、不是完全的超集。
實(shí)戰(zhàn)中的問題:
1、還有一些坑不好解決,axios 編寫了攔截器之后,typescript 反映不到 response 中去。
參考資料
?Typescript 官網(wǎng)[2]?深入理解 Typescript[3]
References
[1]?Typescript 設(shè)計(jì)目標(biāo):?https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals[2]?Typescript 官網(wǎng):?https://www.tslang.cn/[3]?深入理解 Typescript:?https://jkchao.github.io/typescript-book-chinese/
學(xué)習(xí)交流
關(guān)注公眾號(hào)【前端宇宙】,每日獲取好文推薦 添加微信,入群交流
