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 成為新的前端標準」 還是比較看好的。
下面來看一份來國內的 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 初學者都會遇到的疑問。
TypeScript 不能被瀏覽器或 Node.js 和 Deno 這些運行時所理解,最終要編譯為 JavaScript 執行,我們需要一個 compiler 做編譯。另外你可能會想到在 Deno 中不是可以直接寫 TypeScript 嗎,本質上 Deno 也不是直接運行的 TypeScript,同樣需要先編譯為 JavaScript 來運行。
在線編譯想嘗試一下而不想本地安裝的,可以看看以下這兩個在線工具:
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 實現。但兩者還是有些區別:
從概念上每個人都有不同的理解,對於團隊來說無論使用哪一個,都應該保持好統一的規範。在 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 函數交換兩個變量。

Utility type(實用類型)不是一種新的類型,基於類型別名實現的一個工具集的自定義類型。包括:Parameters<T>、Omit<T,K>。
ParametersParameters<T>:接收一個范型 T,這個 T 是一個 Function,將會提取這個函數的返回值為tuple。
例如,兩個函數 fn2 的參數同 fn1 相等,這時使用 Parameters 就很合適。
constfn1=(name:string,age:number)=>{}constfn2=(...[name,age]:Parameters<typeoffn1>)=>{console.log(name,age);}fn2('五月君',18);PritialPritial<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];};PickPick<T,K>:選取范型 T 中指定的部分屬性,如果需要選出多個屬性,用聯合類型指定。
constuser:Pick<Person,'name'>={name:'五月君'};實現原理:首先校驗第二個類型 K 的屬性必須在第一個類型 T 裡面,之後遍歷傳入的聯合類型 K,形成一個新的類型。
typePick<T,KextendskeyofT>={[PinK]:T[P];};OmitOmit<T,K>:與 Pick 相反,將第一個范型 T 中的部分屬性刪除,如果要刪除多個屬性,用聯合類型指定需要刪除的屬性。
constuser:Omit<Person,'age'>={name:'五月君'}//constuser:Omit<Person,'name'|'age'>={}實現原理:Omit 使用了 Pick 和 Exclude 組合來實現的,這個地方有點繞:
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,那我建議你在時間允許的情況,開始做一些嘗試吧。
下面從個人角度,總結一些建議:
-這是底線-
點擊下方卡片關注「編程界」解鎖更多優質內容。