close

TypeScript 是 JavaScript 的超集,JavaScript 能夠做的事情,它都可以做且還增加了很多功能,例如靜態類型、增強的面向對象編程能力等。

本文是筆者日常學習、使用 TypeScript 過程中自己記錄的一些知識點,現在總結分享給大家。包含了做為初學者在學習 TypeScript 時應關注的核心知識,在掌握了這些知識點後,就是在項目中的靈活應用,文末推薦了一個基於 TypeScript 的前後端項目,做為學習可參考(文末閱讀原文查看)。

下圖為本文概覽:

社區發展現狀調研

近些年 TypeScript 在前端圈備受推崇,正在被越來越多的前端開發者所接受,包括在 Node.js 後端開發中同樣如此。

下面從幾份開發者報告調查數據看 TypeScript 的發展現狀。

2022 年前端開發者現狀報告

本報告來源於 https://tsh.io/state-of-frontend/#report 調查面比較廣泛。來自 125 個國家的 3703+ 前端開發者及 19 位前端專家參與了該次調查。

關於 TypeScript ,過去一年裡有將近 84.1% 的參與者表示使用過。

對於 TypeScript 前景描述,近 43% 開發者表示 「TypeScript 將超越 JavaScript 成為新的前端標準」 還是比較看好的。

2021 年 Node.js 年度報告

下面來看一份來國內的 Node.js 2021 年度開發者調查報告 https://nodersurvey.github.io/reporters。

從 「代碼轉譯」 角度調研,有 47.5% 的開發者使用了 TypeScript。在 Node.js 後端框架中 Nest.js 最近幾年也是一個熱門選擇,該框架一個特點是完全基於 TypeScript,受到了更多開發者的青睞。

一些其它的調查者報告數據

還有一些其它的調查報告數據,例如 2021 年 Stack Overflow 調查者報告、Google 搜索量趨勢,NPM 下載量趨勢 從上面可以看出 TypeScript 近幾年的發展趨勢還是很快的。

現在的你可能還在考慮要不要學習 TypeScript,但在將來也許會是每個前端開發者、Node.js 開發者必備的技能。

TS 困擾與收益初學者的 TS 困擾

做為一個初學者剛開始使用 TypeScript 時,你無法在使用一些 JavaScript 的思維來編碼,下面的一部分觀點也許是每一個 TypeScript 初學者都會遇到的疑問。

認為給每一個變量、函數設置類型會拖累代碼編寫速度。
一些難以理解的編譯錯誤,會讓你不知所措,為了能使得項目運行起來,又不得不試圖找出問題所在。
難以理解的范型概念,特別是對於只有 JavaScript 經驗的開發人員。
從未參與過企業級應用程序開發,不知道如何更好的開始。
TS 的收益
類型安全:類比 Java 這些強類型語言,通過類型檢查也可及早發現問題。
增強的面向對象能力:支持面向對象的封裝、繼承、多態三大特性
類似 babel:ES6 ES7 新語法都可以寫,最終 TS 會進行編譯。
生產力工具的提升:VS Code + TS 使 IDE 更容易理解你的代碼。
靜態代碼分析能力:解決原先在運行時(runtime) JavaScript 無法發現的錯誤。
易於項目後期維護、重構:當版本迭代時,比如當我們為一個函數新增加一個參數屬性或為一個類型增加狀態,如果忘記更新引用的地方,編譯器就會給予我們警告或錯誤提示。
開發環境搭建

TypeScript 不能被瀏覽器或 Node.js 和 Deno 這些運行時所理解,最終要編譯為 JavaScript 執行,我們需要一個 compiler 做編譯。另外你可能會想到在 Deno 中不是可以直接寫 TypeScript 嗎,本質上 Deno 也不是直接運行的 TypeScript,同樣需要先編譯為 JavaScript 來運行。

在線編譯

想嘗試一下而不想本地安裝的,可以看看以下這兩個在線工具:

www.typescriptlang.org/play:這個是 TypeScript 官網提供的在線編譯運行,可將 TS 代碼編譯為 JS 代碼執行。
codesandbox.io:這個工具支持的框架很多,包括前端的 React、服務端的 Nest.js 等,在這裡練手 TypeScript 也是可以的。
tsc VS ts-node

tsc 是將 TypeScript 代碼編譯為 JavaScript 代碼,全局安裝npm install -g typescript即可得到一個 tsc 命令,之後通過tsc hello.ts編譯 typescript 文件。

//編譯前hello.tsconstmessage:string='HelloNodejs';//編譯後hello.jsvarmessage='HelloNodejs';

與 tsc 不同的是ts-node是編譯 + 執行。可以在開發時使用 ts-node,生產環境使用 tsc 編譯。

#安裝全局依賴$npminstall-gtypescript$npminstall-gts-node#運行$ts-nodehello.ts框架/庫支持

使用 CRA、Vite 這種庫創建出來的前端 TS 項目、及後端 Nest.js 這樣的框架,默認都是支持 TypeScript 的,一些基礎的 tsconfig.json 配置文件,也都幫你配置好了。

初始化配置文件

TypeScript 編譯時會使用tsconfig.json文件做為配置文件,該配置所在的項目會被認為是根目錄。

使用npx tsc --init命令快速創建一個 tsconfig.json 配置文件,具體的配置可參考 文檔。

數據類型核心概念

TypeScript 除了包含 JavaScript 已有的string、number、boolean、symbol、bigint、undefined、null、array、object數據類型之外,還包括tuple、enum、any、unknown、never、void、范型概念及類型聲明符號ineterface、type。

基礎數據類型

在參數名稱後面使用冒號指定參數類型,同時也可在類型後面賦默認值,const 聲明的變量必須要賦予默認值否則 IDE 編譯器會提示錯誤,let 則不是必須的。

對於一些基礎的數據類型,如果後面有值,TS 可以自動進行類型推斷,不需要顯示聲明,例如const nickname: string = '五月君'等價於const nickname = '五月君'。

constnickname:string='五月君';//字符串constage:number=20;//Number類型constman:boolean=true;//布爾型lethobby:string;//字符串僅聲明一個變量leta:undefined=undefined;//undefined類型letb:null=null;//null類型,TS區分了undefined、nullletlist:any[]=[1,true,"free"];//不需要類型檢查器檢測直接通過編譯階段檢測的可以使用any,但是這樣和直接使用JavaScript沒什麼區別了letc:any;c=1;c='1';數組 VS 元組

數組(Array)通常用來表示所有元素類型相同的集合,也可以使用數組泛型:Array<element type>允許這個集合中存在多種類型。

constlist1:number[]=[1,2,3];constlist2:Array<number|string>=[1,'2',3];

元組(Tuple)允許一個已知元素數量的數組中各元素的類型可以是不同的,通常是事先定義好的不能被修改,元組的定義和賦值必須要一一對應。

constlist1:[number,string,boolean]=[1,'2',true];//正確constlist2:[number,string,boolean]=[1,2,true];//元素2會報錯,不能將類型"number"分配給類型"string"

元組更嚴格一些,不可出現越界操作,例如 list1 只有三個元素,下標從 0 ~ 2,如果執行 list1[3] 就會報錯,如下所示,這在數組操作中是不會出現報錯的。

list1[3]Tupletype'[number,string,boolean]'oflength'3'hasnoelementatindex'3'.函數類型聲明

可選參數使用 「?」 符號聲明,可選參數、函數參數的默認值需要聲明在必選參數之後。函數也可以聲明返回值類型,在某些情況下不需要顯示聲明,能夠自動推斷出返回值類型。

當一個函數沒有返回值時用 void 表示,在 JavaScript 中一個函數沒有返回值,它的結果也等同於 undefined。

//定義函數返回值為空//給傳入的參數定義類型//給傳入的參數賦予默認值constfn=function(content:string='Hello',nickname?:string):void{console.log(content,nickname);}fn();//Helloundefinedfn('Hello','五月君');//Hello五月君//指定函數的返回值為stringfunctionfn():string{return'str';}//根據傳入的參數可以自動推斷類型constadd=(a:number,b:number)=>{returna+b;}任何值 - any? 還是 unknown?

俗話說:「一入 any 深似海,從此類型是路人」,any 不會做任何類型檢查,下面這段代碼運行之後肯定會報TypeError: value.toLocaleLowerCase is not a function錯誤,並且這種錯誤只能在運行時才會發現,應儘可能的避免使用 any,否則就失去了使用 TypeScript 的意義。

constfn=(value:any)=>{value.toLocaleLowerCase();}fn(1);

unknown 也表示任何值,相比於 any 它更加嚴謹,在使用上會有很多限制。

下面代碼在編譯時fn1函數會報錯TSError: ⨯ Unable to compile TypeScript: Object is of type 'unknown'。

constfn1=(value:unknown)=>{value.toLocaleLowerCase();//編譯失敗}constfn2=(value?:unknown)=>{returntypeofvalue==='string';//編譯通過}fn1(1);fn2(1);

不對 unknown 聲明的類型做任何取值操作,是沒問題的,正如上例的fn2函數。這個時候就有疑問了,既然什麼都不能操作,有什麼應用場景呢?

unknown 與類型守衛

unknown 的意義在於我們可以結合 「類型守衛」 在聲明的函數或其它塊級作用域內獲取精確的參數類型。這樣在運行時也能避免出現類型錯誤這種常見問題。

例如,想對一個數組執行length操作時,首先通過Array.isArray進一步精確參數的類型之後,在做一些操作。

constfn=(value?:unknown)=>{if(Array.isArray(value)){returnvalue.length;//編譯通過}}

使用 is 關鍵詞自定義類型守衛{參數名稱} is {參數類型},這個意思是告訴 TypeScript 函數 isString() 返回的是一個 string 類型。

functionisString(str:unknown):strisstring{returntypeofstr==='string';}constfn=(value?:unknown)=>{if(isString(value)){returnvalue.toLocaleLowerCase();//編譯通過}}

例如,react-query 這個庫返回的 error 默認為 unknown 類型,如果在 render 時直接這樣寫<p>${error.message}<p>是不行的,一個解決方案是拿到錯誤後用類型守衛處理,如下所示:

constisError=(error:unknown):errorisError=>{returnerrorinstanceofError;};<p>Error:{isError(error)&&error.message}</p>

另外一種方案是在調用它的 Api 時直接傳入一個 Error 對象:useMutation<TResponse, Error, string>()

枚舉

枚舉定義了一組相關值的集合,通過描述性的常量聲明使代碼更具可讀性。默認情況下枚舉將字符串的值存儲為從 0 開始的數字,也可以顯示聲明為字符串。

enumOrderStatus{CREATED,//0CANCELLED,//1COMPLETED,//2}enumOrderStatus{CREATED='created',CANCELLED='cancelled',COMPLETED='completed',}交叉、聯合類型、類型別名

交叉類型:是將多個類型合併為一個類型,使用符號 & 表示。例如,將 TPerson & TWorker 合併為一個新的類型 User。

interfaceTPerson{name:string,age:number,}interfaceTWorker{jobTitle:string,}typeUser=TPerson&TWorker;constuser:User={name:'Tom',age:18,jobTitle:'Developer'}

注意,聲明類型時不要與系統的關鍵詞衝突,例如上例中的 Worker 儘管使用 interface 聲明時沒有提示報錯,但使用時會有提示,因此才改為 TWorker。

聯合類型:表示一個變量通常由多個類型組成,這之間是一種或的關係,使用|符號聲明。意思是 id 即可以是 string 也可以是 number 類型。

letid:string|number;

類型別名:給一個類型起一個新名字。如果另外一個字段和 id 一樣也是由相同的多個類型組成,就要用到類型別名了,使用type關鍵字將多個基本類型聲明為一個自定義的類型,這種是interface替代不了的。

typeStringOrNumber=string|number;letid:StringOrNumber;letno:StringOrNumber;class、interface、type 類型聲明

TypeScript 中使用 class、interface、type 關鍵詞均可聲明類型。class 聲明的是一個類對象,這個好理解,容易迷惑的地方在於 interface 和 type 兩者分別該用於何處。

//class聲明類型classPerson{nickname:string;age:number;}//interface聲明類型interfacePerson{nickname:string;age:number;}//type聲明類型typePerson={nickname:string;age:number;}

interface 和 type 非常相似,大多數情況下 interface 的特性都可以使用 type 實現。但兩者還是有些區別:

interface:用於聲明對象的行為,描述對象的屬性、方法,可以被繼承(extends)、實現(implements)不能用於定義基本類型,如果你想聲明一個接口,用 interface 就好了。
type:可以用於聲明基本類型,儘管 type 也可以聯合多個類型,但 type 不是真正的 extends,而是使用一些操作符實現的類型合併。

從概念上每個人都有不同的理解,對於團隊來說無論使用哪一個,都應該保持好統一的規範。在 TypeScript 官網文檔中也提到了 如果不清楚該使用哪一個,請使用 interface 直到您需要 type。

鴨子類型

鴨子類型在程序設計中是動態類型的一種風格,它的關注點在於對象的行為能做什麼,而不是關注對象所屬的類型。這個概念的名字源自一個 「鴨子測試」,可以解釋為 :

「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。」

下例,使用 interface 定義了兩個類型 Duck、Bird,它們都有共同的特徵 「走路」,於是對兩個類型分別定義了 walk() 方法。test() 方法中期望的 value 類型為 Duck,但實際調用時傳的 value 是 Bird,在 TypeScript 中是可以編譯通過的。

interfaceDuck{walk:()=>void;}interfaceBird{walk:()=>void;}consttest=(value:Duck)=>{value.walk();//輸出「bird」測試通過}constvalue:Bird={walk:()=>{console.log('bird');}}test(value);

TypeScript 的鴨子類型是面向接口編程,雖然我們的例子 Duck、Bird 類型不一樣,但是都有共同的接口,是可以編譯通過的,對於面向對象編程的語言,例如 Java 就不行了。

范型

在參數和返回類型未知的情況下,可以使用范型來傳遞類型,保證組件內的類型安全。

一個有用的場景是用范型定義接口返回對象。例如,接口返回值 Result 對象,每一個接口返回的 data 是不一樣的,這時 Result 對象的 data 屬性就適合用范型作為類型進行傳遞。

interfaceResult<T>={code:string;message:string;data:T;}interfaceUser={userId:string;}constusers:Result<User[]>={//獲取用戶列表接口返回值code:'SUCCESS',message:'Requestsuccess',data:[{userId:'123',}]}constuserInfo:Result<User>={//獲取用戶詳情接口返回值code:'SUCCESS',message:'Requestsuccess',data:{userId:'123',}}

在函數內部使用范型變量時,存在類型約束,不能隨意操作它的屬性。例如下例,需要事先定義范型 T 擁有 length 屬性。

typeTLenght={length:number;}functionfn<TextendsTLenght>(val:T){if(val.length){//dosomething}}fn('hello')//5fn([1,2])//2fn({length:3})//3

范型定義也可以有多個。例如,swap 函數交換兩個變量。

image.png實用類型工具

Utility type(實用類型)不是一種新的類型,基於類型別名實現的一個工具集的自定義類型。包括:Parameters<T>、Omit<T,K>。

Parameters

Parameters<T>:接收一個范型 T,這個 T 是一個 Function,將會提取這個函數的返回值為tuple。

例如,兩個函數 fn2 的參數同 fn1 相等,這時使用 Parameters 就很合適。

constfn1=(name:string,age:number)=>{}constfn2=(...[name,age]:Parameters<typeoffn1>)=>{console.log(name,age);}fn2('五月君',18);Pritial

Pritial<T>:將范型 T 的所有屬性變為可選。例如,user1 我必須寫 name、age,而 user2 就不用了,現在都變為選填了,只填寫需要的數據。

interfacePerson{name:string,age:number,}constuser1:Person={name:'五月君',age:18};constuser2:Partial<Person>={};

實現原理:實用 keyof 關鍵詞取出 T 的所有的屬性,遍歷過程為每個屬性加了一個?符號,也就表示可選的。

typePartial<T>={[PinkeyofT]?:T[P];};Pick

Pick<T,K>:選取范型 T 中指定的部分屬性,如果需要選出多個屬性,用聯合類型指定。

constuser:Pick<Person,'name'>={name:'五月君'};

實現原理:首先校驗第二個類型 K 的屬性必須在第一個類型 T 裡面,之後遍歷傳入的聯合類型 K,形成一個新的類型。

typePick<T,KextendskeyofT>={[PinK]:T[P];};Omit

Omit<T,K>:與 Pick 相反,將第一個范型 T 中的部分屬性刪除,如果要刪除多個屬性,用聯合類型指定需要刪除的屬性。

constuser:Omit<Person,'age'>={name:'五月君'}//constuser:Omit<Person,'name'|'age'>={}

實現原理:Omit 使用了 Pick 和 Exclude 組合來實現的,這個地方有點繞:

Exclude 的第一個類型 T 是一個聯合類型,T extends U ? never : T這塊的判斷是,如果類型匹配,返回 never 就什麼也沒有,不匹配則返回。我們的例子中,age 類型匹配沒有返回,返回的是未匹配的 name。
再通過 Pick 取出 Exclude 的結果,這樣也就相當於達到了過濾的效果。
/***ExcludefromTthosetypesthatareassignabletoU*/typeExclude<T,U>=TextendsU?never:T;typeOmit<T,Kextendskeyofany>=Pick<T,Exclude<keyofT,K>>;//下面兩個是等價的constuser:Omit<Person,'name'|'age'>={}//等價於constuser:Pick<Person,Exclude<'name'|'age','age'>>={name:'五月君'}增強的面向對象能力

ES6 時代為 JavaScript 增加了 class 「語法糖」,可以方便的去定義一個類並通過 extends 關鍵詞實現繼承,儘管 ES6 中的 class 本質上仍是基於原型鏈實現的,但代碼編寫方式看起來簡潔多了(以 class 關鍵詞進行的面向對象編程)。

和其它的面向對象編程語言相比較,會發現 JavaScript 中的 class 少了好多功能。一個常見需求是不能私有化類成員,為了達到這個目的,通常有幾種做法:在屬性或方法前加上_表示私有化,這屬於命名規則約束、使用 symbol 的唯一性實現私有化。

TypeScript 中增強了面向對象的編程能力,具備類的訪問權限控制、接口、模塊、類型註解等功能。

類成員訪問權限控制

對象的成員屬性或方法如果沒有被封裝,實例化後在外部就可通過引用獲取,對於用戶 phone 這種數據,是不能隨意被別人獲取的。

封裝性做為面向對象編程重要特性之一,它是把類內部成員屬性、成員方法統一保護起來,只保留有限的接口與外部進行聯繫,儘可能屏蔽對象的內部細節,防止外部隨意修改內部數據,保證數據的安全性。

同傳統的面向對象編程語言類似,TypeScript 提供了 3 個關鍵詞 public、private、protected 控制類成員的訪問權限。

classPerson{publicname:string;//屬性「name」可以被外部調用protectedemail:string;//屬性「email」受保護,只能在類「Person」及其子類中訪問privatephone:string;//屬性「phone」為私有屬性,只能在類「Person」中訪問。constructor(name:string,email:string,phone:string){this.name=name;this.email=email;this.phone=phone;}publicinfo(){console.log(`我是${this.name}手機號${this.formatPhone()}`)}privateformatPhone(){//方法「formatPhone」為私有屬性,只能在類「Person」中訪問。returnthis.phone.replace(/(\d{3})\d{4}(\d{3})/,'$1****$2');}}接口

接口是一種特殊的抽象類,與抽象類不同的是,接口沒有具體的實現,只有定義,通過 interface 關鍵詞聲明。

TypeScript 對接口的定義是這樣的:

TypeScript 的核心原則之一是對值所具有的結構進行類型檢查。它有時被稱做 「鴨式辨型法」 或 「結構性子類型化」。在 TypeScript 里,接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約。

TypeScript 中只能單繼承,但可以實現多個接口。

interfacePerson{name:string;phone?:string;}interfaceStudent{diploma():void;}classHighSchoolimplementsStudent,Person{name:string;diploma():void{console.log('高中');}}classUniversityimplementsStudent,Person{name:string;diploma():void{console.log('大學本科')}}

面向對象程序設計概念不止這些,參見這篇文章 https://github.com/qufei1993/blog/issues/41。

總結用還是不用?

最終要不要用 TypeScript,還要結合項目規模、維護周期、團隊成員多方面看,以下為個人的一些理解:

如果是一些產品的核心項目,維護周期長、參與人員多,TypeScript 是可以嘗試的,可能前期會感覺定義類型約束很多,對於後期的維護、重構是有一定幫助的。
對於一些項目周期不長或是一些小項目,不想被 TypeScript 的類型約束所束縛,可以選擇 JavaScript。
不能規避的一個現實問題是 TypeScript 是有一些上手成本的,看團隊成員的情況,公司內的產品項目不是一個人單打獨鬥,開發是一個階段,後期還需要大家一起維護,可以看大家的意願,對 TypeScript 了解程度,是否願意學習嘗試。
該怎麼學習?

文章開頭我們看了一些 TypeScript 社區發展現狀調研報告,從目前使用情況、發展趨勢看,已然成為前端開發者的必備技能之一。如果你還在猶豫要不要學習 TypeScript,那我建議你在時間允許的情況,開始做一些嘗試吧。

下面從個人角度,總結一些建議:

關注點一開始不要太放在工具上,選擇一個穩定的編譯工具,例如 tsc、ts-node,這些就夠了。之後可以嘗試一些性能更好的編譯工具。
先了解一些 TypeScript 增強的基礎類型和類型約束,例如 枚舉、any VS unkonwn、數組 VS 元組及類型聲明符 type、interface 等,先入門在進階。不建議剛上來就搞一些很高級的操作,例如 「類型體操」。
從一個實際項目開始、如果公司正在實踐 TypeScript 這是一個很好的學習機會。如果公司沒有用也沒關係,社區上也有很多 TypeScript 的開源項目,都可以做為參考,例如寫一個博客 參考 koala 的 https://github.com/koala-coding/nest-blog,筆者之前基於 TypeScript 寫了一個前後端的開源項目 https://github.com/qufei1993/compressor,這些都可以做為參考學習(如果有幫助,別忘記給一個 star 哈,這就是給予作者的最大支持了!)
多看官方的文檔 https://www.typescriptlang.org/ 即使英語不好,也可以嘗試着閱讀下,這是一手的學習資料,實在有困難的可以去看中文文檔,遇到問題多 Google。
學會總結分享,這是學習所有知識通用的方法。在寫過一個項目後,多多少少都會遇到一些問題,日常還是還要善於總結,這就是一種知識的沉澱和自我積累。目前你看到本文,並不是筆者一口氣寫完的,中間的一部分也是日常學習、使用 TypeScript 過程中自己記錄的一些知識點,現在總結分享給大家,自己也會加深印象。

-這是底線-

點擊下方卡片關注「編程界」解鎖更多優質內容。


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()