大家好,我是ConardLi,在過去的幾年裡TypeScript變得越來越流行,現在許多工作都要求開發人員了解TypeScript,各大廠的大型項目基本都要求使用TypeScript編寫。
如果你已經對JavaScript很熟了,TypeScript基本上也能快速上手,下面是我整理的一些初學者必備的一些知識點,如果你已經是個TS高手了,可以期待我後續的文章了~
Typescript 簡介
據官方描述:TypeScript是JavaScript的超集,這意味着它可以完成JavaScript所做的所有事情,而且額外附帶了一些能力。
JavaScript本身是一種動態類型語言,這意味着變量可以改變類型。使用TypeScript的主要原因是就是為了給JavaScript添加靜態類型。靜態類型意味着變量的類型在程序中的任何時候都不能改變。它可以防止很多bug !
Typescript 值得學嗎?
下面是學習Typescript的幾個理由:
當然,使用Typescript也有一些缺點:
但是,相比於提前發現更多的 bug,花更長的時間也是值得的。
TypeScript 中的類型原始類型
在JavaScript中,有 7 種原始類型:
原始類型都是不可變的,你可以為原始類型的變量重新分配一個新值,但不能像更改對象、數組和函數一樣更改它的值。可以看下面的例子:
letname='ConardLi';name.toLowerCase();console.log(name);//ConardLi-字符串的方法並沒有改變字符串本身letarr=[1,3,5,7];arr.pop();console.log(arr);//[1,3,5]-數組的方法改變了數組
回到TypeScript,我們可以在聲明一個變量之後設置我們想要添加的類型:type(我們一般稱之為「類型注釋」或「類型簽名」):
letid:number=5;letfirstname:string='ConardLi';lethasDog:boolean=true;letunit:number;//聲明變量而不賦值unit=5;
但是,如果變量有默認值的話,一般我們也不需要顯式聲明類型,TypeScript會自動推斷變量的類型(類型推斷):
letid=5;//number類型letfirstname='ConardLi';//string類型lethasDog=true;//boolean類型hasDog='yes';//ERROR
我們還可以將變量設置為聯合類型(聯合類型是可以分配多個類型的變量):
letage:string|number;age=17;age='17';TypeScript 中的數組
在TypeScript中,你可以定義數組包含的數據類型:
letids:number[]=[1,2,3,4,5];//只能包含numberletnames:string[]=['ConardLi','Tom','Jerry'];//只能包含stringletoptions:boolean[]=[true,false,false];只能包含truefalseletbooks:object[]=[{name:'Tom',animal:'cat'},{name:'Jerry',animal:'mouse'},];//只能包含對象letarr:any[]=['hello',1,true];//啥都行,回到了JSids.push(6);ids.push('7');//ERROR:Argumentoftype'string'isnotassignabletoparameteroftype'number'.
你也可以使用聯合類型來定義包含多種類型的數組:
letperson:(string|number|boolean)[]=['ConardLi',1,true];person[0]=100;person[1]={name:'ConardLi'}//Error-personarraycan'tcontainobjects
如果數組有默認值,TypeScript同樣也會進行類型推斷:
letperson=['ConardLi',1,true];//和上面的例子一樣person[0]=100;person[1]={name:'ConardLi'};//Error-personarraycan'tcontainobjects
TypeScript中可以定義一種特殊類型的數組:元組(Tuple)。元組是具有固定大小和已知數據類型的數組,它比常規數組更嚴格。
letperson:[string,number,boolean]=['ConardLi',1,true];person[0]=17;//Error-Valueatindex0canonlybeastringTypeScript 中的對象
TypeScript中的對象必須擁有所有正確的屬性和值類型:
//使用特定的對象類型注釋聲明一個名為person的變量letperson:{name:string;age:number;isProgrammer:boolean;};//給person分配一個具有所有必要屬性和值類型的對象person={name:'ConardLi',age:17,isProgrammer:true,};person.age='17';//ERROR:shouldbeanumberperson={name:'Tom',age:3,};//ERROR:missingtheisProgrammerproperty
在定義對象的類型時,我們通常會使用interface。如果我們需要檢查多個對象是否具有相同的特定屬性和值類型時,是很有用的:
interfacePerson{name:string;age:number;isProgrammer:boolean;}letperson1:Person={name:'ConardLi',age:17,isProgrammer:true,};letperson2:Person={name:'Tom',age:3,isProgrammer:false,};
我們還可以用函數的類型簽名聲明一個函數屬性,通用函數(sayHi)和箭頭函數(sayBye)都可以聲明:
interfaceAnimal{eat(name:string):string;speak:(name:string)=>string;}lettom:Animal={eat:function(name:string){return`eat${name}`;},speak:(name:string)=>`speak${name}`,};console.log(tom.eat('Jerry'));console.log(tom.speak('哈哈哈'));
需要注意的是,雖然eat、speak分別是用普通函數和箭頭函數聲明的,但是它們具體是什麼樣的函數類型都可以,Typescript是不關心這些的。
TypeScript 中的函數
我們可以定義函數參數和返回值的類型:
//定義一個名為circle的函數,它接受一個類型為number的直徑變量,並返回一個字符串functioncircle(diam:number):string{return'圓的周長為:'+Math.PI*diam;}console.log(circle(10));//圓的周長為:31.41592653589793
ES6 箭頭函數的寫法:
constcircle=(diam:number):string=>{return'圓的周長為:'+Math.PI*diam;};
我們沒必要明確聲明circle是一個函數,TypeScript會進行類型推斷。TypeScript還會推斷函數的返回類型,但是如果函數體比較複雜,還是建議清晰的顯式聲明返回類型。
我們可以在參數後添加一個?,表示它為可選參數;另外參數的類型也可以是一個聯合類型:
constadd=(a:number,b:number,c?:number|string)=>{console.log(c);returna+b;};console.log(add(5,4,'可以是number、string,也可以為空'));
如果函數沒有返回值,在TS里表示為返回void,你也不需要顯式聲明,TS一樣可以進行類型推斷:
constlog=(msg:string):void=>{console.log('打印一些內容:'+msg);};any 類型
使any類型,我們基本上可以將TypeScript恢復為JavaScript:
letname:any='ConardLi';name=17;name={age:17};
如果代碼里使用了大量的any,那TypeScript也就失去了意義,所以我們應該儘量避免使用any。
DOM 和類型轉換
TypeScript沒辦法像JavaScript那樣訪問DOM。這意味着每當我們嘗試訪問DOM元素時,TypeScript都無法確定它們是否真的存在。
constlink=document.querySelector('a');console.log(link.href);//ERROR:Objectispossibly'null'.TypeScriptcan'tbesuretheanchortagexists,asitcan'taccesstheDOM
使用非空斷言運算符 (!),我們可以明確地告訴編譯器一個表達式的值不是null或undefined。當編譯器無法準確地進行類型推斷時,這可能很有用:
//我們明確告訴TSa標籤肯定存在constlink=document.querySelector('a')!;console.log(link.href);//conardli.top
這裡我們沒必要聲明link變量的類型。這是因為TypeScript可以通過類型推斷確認它的類型為HTMLAnchorElement。
但是如果我們需要通過class或id來選擇一個DOM元素呢?這時TypeScript就沒辦法推斷類型了:
constform=document.getElementById('signup-form');console.log(form.method);//ERROR:Objectispossibly'null'.//ERROR:Property'method'doesnotexistontype'HTMLElement'.
我們需要告訴TypeScriptform確定是存在的,並且我們知道它的類型是 HTMLFormElement。我們可以通過類型轉換來做到這一點:
constform=document.getElementById('signup-form')asHTMLFormElement;console.log(form.method);//post
TypeScript還內置了一個Event對象。如果我們在表單中添加一個submit的事件偵聽器,TypeScript可以自動幫我們推斷類型錯誤:
constform=document.getElementById('signup-form')asHTMLFormElement;form.addEventListener('submit',(e:Event)=>{e.preventDefault();//阻止頁面刷新console.log(e.tarrget);//ERROR:Property'tarrget'doesnotexistontype'Event'.Didyoumean'target'?});TypeScript 中的類
我們可以定義類中每條數據的類型:
classPerson{name:string;isCool:boolean;age:number;constructor(n:string,c:boolean,a:number){this.name=n;this.isCool=c;this.age=a;}sayHello(){return`Hi,我是${this.name},我今年${this.age}歲了`;}}constperson1=newPerson('ConardLi',true,17);constperson2=newPerson('Jerry','yes',20);//ERROR:Argumentoftype'string'isnotassignabletoparameteroftype'boolean'.console.log(person1.sayHello());//Hi,我是ConardLi,我今年17歲了
我們可以創建一個僅包含從Person構造的對象數組:
letPeople:Person[]=[person1,person2];
我們可以給類的屬性添加訪問修飾符,TypeScript還提供了一個新的readonly訪問修飾符。
classPerson{readonlyname:string;//不可以變的privateisCool:boolean;//類的私有屬性、外部訪問不到protectedemail:string;//只能從這個類和子類中進行訪問和修改publicage:number;//任何地方都可以訪問和修改constructor(n:string,c:boolean,a:number){this.name=n;this.isCool=c;this.age=a;}sayHello(){return`Hi,我是${this.name},我今年${this.age}歲了`;}}constperson1=newPerson('ConardLi',true,'conard@xx.com',17);console.log(person1.name);//ConardLiperson1.name='Jerry';//Error:readonly
我們可以通過下面的寫法,屬性會在構造函數中自動分配,我們類會更加簡潔:
classPerson{constructor(readonlyname:string,privateisCool:boolean,protectedemail:string,publicage:number){}}
如果我們省略訪問修飾符,默認情況下屬性都是public,另外和 JavaScript 一樣,類也是可以extends的。
TypeScript 中的接口
接口定義了對象的外觀:
interfacePerson{name:string;age:number;}functionsayHi(person:Person){console.log(`Hi${person.name}`);}sayHi({name:'ConardLi',age:17,});//HiConardLi
你還可以使用類型別名定義對象類型:
typePerson={name:string;age:number;};
或者可以直接匿名定義對象類型:
functionsayHi(person:{name:string;age:number}){console.log(`Hi${person.name}`);}
interface和type非常相似,很多情況下它倆可以隨便用。比如它們兩個都可以擴展:
擴展interface:
interfaceAnimal{name:string}interfaceBearextendsAnimal{honey:boolean}constbear:Bear={name:"Winnie",honey:true,}
擴展type:
typeAnimal={name:string}typeBear=Animal&{honey:boolean}constbear:Bear={name:"Winnie",honey:true,}
但是有個比較明顯的區別,interface是可以自動合併類型的,但是type不支持:
interfaceAnimal{name:string}interfaceAnimal{tail:boolean}constdog:Animal={name:"Tom",tail:true,}
類型別名在創建後無法更改:
typeAnimal={name:string}typeAnimal={tail:boolean}//ERROR:Duplicateidentifier'Animal'.
一般來說,當你不知道用啥的時候,默認就用interface就行,直到interface滿足不了我們的需求的時候再用type。
類的 interface
我們可以通過實現一個接口來告訴一個類它必須包含某些屬性和方法:
interfaceHasFormatter{format():string;}classPersonimplementsHasFormatter{constructor(publicusername:string,protectedpassword:string){}format(){returnthis.username.toLocaleLowerCase();}}letperson1:HasFormatter;letperson2:HasFormatter;person1=newPerson('ConardLi','admin123');person2=newPerson('Tom','admin123');console.log(person1.format());//conardli
確保people是一個實現HasFormatter的對象數組(確保每people都有format方法):
letpeople:HasFormatter[]=[];people.push(person1);people.push(person2);泛型
泛型可以讓我們創建一個可以在多種類型上工作的組件,它能夠支持當前的數據類型,同時也能支持未來的數據類型,這大大提升了組件的可重用性。我們來看下面這個例子:
addID函數接受一個任意對象,並返回一個新對象,其中包含傳入對象的所有屬性和值,以及一個0到1000之間隨機的id屬性。
constaddID=(obj:object)=>{letid=Math.floor(Math.random()*1000);return{...obj,id};};letperson1=addID({name:'John',age:40});console.log(person1.id);//271console.log(person1.name);//ERROR:Property'name'doesnotexistontype'{id:number;}'.
當我們嘗試訪問name屬性時,TypeScript會出錯。這是因為當我們將一個對象傳遞給addID時,我們並沒有指定這個對象應該有什麼屬性 —— 所以TypeScript不知道這個對象有什麼屬性。因此,TypeScript知道的唯一屬性返回對象的id。
那麼,我們怎麼將任意對象傳遞給addID,而且仍然可以告訴TypeScript該對象具有哪些屬性和值?這種場景就可以使用泛型了,<T>–T被稱為類型參數:
//<T>只是一種編寫習慣-我們也可以用<X>或<A>constaddID=<T>(obj:T)=>{letid=Math.floor(Math.random()*1000);return{...obj,id};};
這是啥意思呢?現在當我們再將一個對象傳遞給addID時,我們已經告訴TypeScript來捕獲它的類型了 —— 所以T就變成了我們傳入的任何類型。addID現在會知道我們傳入的對象上有哪些屬性。
但是,現在有另一個問題:任何東西都可以傳入addID,TypeScript將捕獲類型而且並不會報告問題:
letperson1=addID({name:'ConardLi',age:17});letperson2=addID('Jerry');//傳遞字符串也沒問題console.log(person1.id);//188console.log(person1.name);//ConardLiconsole.log(person2.id);console.log(person2.name);//ERROR:Property'name'doesnotexistontype'"Jerry"&{id:number;}'.
當我們傳入一個字符串時,TypeScript沒有發現任何問題。只有我們嘗試訪問name屬性時才會報告錯誤。所以,我們需要一個約束:我們需要通過將泛型類型T作為object的擴展,來告訴TypeScript只能接受對象:
constaddID=<Textendsobject>(obj:T)=>{letid=Math.floor(Math.random()*1000);return{...obj,id};};letperson1=addID({name:'John',age:40});letperson2=addID('Jerry');//ERROR:Argumentoftype'string'isnotassignabletoparameteroftype'object'.
錯誤馬上就被捕獲了,完美…… 好吧,也不完全是。在JavaScript中,數組也是對象,所以我們仍然可以通過傳入數組來逃避類型檢查:
letperson2=addID(['ConardLi',17]);//傳遞數組沒問題console.log(person2.id);//188console.log(person2.name);//Error:Property'name'doesnotexistontype'(string|number)[]&{id:number;}'.
要解決這個問題,我們可以這樣說:object參數應該有一個帶有字符串值的name屬性:
constaddID=<Textends{name:string}>(obj:T)=>{letid=Math.floor(Math.random()*1000);return{...obj,id};};letperson2=addID(['ConardLi',17]);//ERROR:argumentshouldhaveanamepropertywithstringvalue
泛型允許在參數和返回類型提前未知的組件中具有類型安全。
在TypeScript中,泛型用於描述兩個值之間的對應關係。在上面的例子中,返回類型與輸入類型有關。我們用一個泛型來描述對應關係。
另一個例子:如果需要接受多個類型的函數,最好使用泛型而不是 any 。下面展示了使用any的問題:
functionlogLength(a:any){console.log(a.length);//Noerrorreturna;}lethello='Helloworld';logLength(hello);//11lethowMany=8;logLength(howMany);//undefined(butnoTypeScripterror-surelywewantTypeScripttotelluswe'vetriedtoaccessalengthpropertyonanumber!)
我們可以嘗試使用泛型:
functionlogLength<T>(a:T){console.log(a.length);//ERROR:TypeScriptisn'tcertainthat`a`isavaluewithalengthpropertyreturna;}
好,至少我們現在得到了一些反饋,可以幫助我們持續改進我們的代碼。
解決方案:使用一個泛型來擴展一個接口,確保傳入的每個參數都有一個length屬性:
interfacehasLength{length:number;}functionlogLength<TextendshasLength>(a:T){console.log(a.length);returna;}lethello='Helloworld';logLength(hello);//11lethowMany=8;logLength(howMany);//Error:numbersdon'thavelengthproperties
我們也可以編寫這樣一個函數,它的參數是一個元素數組,這些元素都有一個length屬性:
interfacehasLength{length:number;}functionlogLengths<TextendshasLength>(a:T[]){a.forEach((element)=>{console.log(element.length);});}letarr=['Thisstringhasalengthprop',['This','arr','has','length'],{material:'plastic',length:17},];logLengths(arr);//29//4//30
泛型是TypeScript的一個很棒的特性!
泛型接口
當我們不知道對象中的某個值是什麼類型時,可以使用泛型來傳遞該類型:
//Thetype,T,willbepassedininterfacePerson<T>{name:string;age:number;documents:T;}//Wehavetopassinthetypeof`documents`-anarrayofstringsinthiscaseconstperson1:Person<string[]>={name:'ConardLi',age:17,documents:['passport','bankstatement','visa'],};//Again,weimplementthe`Person`interface,andpassinthetypefordocuments-inthiscaseastringconstperson2:Person<string>={name:'Tom',age:20,documents:'passport,P45',};枚舉
枚舉是TypeScript給JavaScript帶來的一個特殊特性。枚舉允許我們定義或聲明一組相關值,可以是數字或字符串,作為一組命名常量。
enumResourceType{BOOK,AUTHOR,FILM,DIRECTOR,PERSON,}console.log(ResourceType.BOOK);//0console.log(ResourceType.AUTHOR);//1//從1開始enumResourceType{BOOK=1,AUTHOR,FILM,DIRECTOR,PERSON,}console.log(ResourceType.BOOK);//1console.log(ResourceType.AUTHOR);//2
默認情況下,枚舉是基於數字的 — 它們將字符串值存儲為數字。但它們也可以是字符串:
enumDirection{Up='Up',Right='Right',Down='Down',Left='Left',}console.log(Direction.Right);//Rightconsole.log(Direction.Down);//Down
當我們有一組相關的常量時,枚舉就可以派上用場了。例如,與在代碼中使用非描述性數字不同,枚舉通過描述性常量使代碼更具可讀性。
枚舉還可以防止錯誤,因為當你輸入枚舉的名稱時,智能提示將彈出可能選擇的選項列表。
TypeScript 嚴格模式
建議在tsconfig.json中啟用所有嚴格的類型檢查操作文件。這可能會導致TypeScript報告更多的錯誤,但也更有助於幫你提前發現發現程序中更多的bug。
//tsconfig.json"strict":true
嚴格模式實際上就意味着:禁止隱式 any 和 嚴格的空檢查。
禁止隱式 any
在下面的函數中,TypeScript已經推斷出參數a是any類型的。當我們向該函數傳遞一個數字,並嘗試打印一個name屬性時,沒有報錯:
functionlogName(a){//Noerror??console.log(a.name);}logName(97);
打開noImplicitAny選項後,如果我們沒有顯式地聲明a的類型,TypeScript將立即標記一個錯誤:
//ERROR:Parameter'a'implicitlyhasan'any'type.functionlogName(a){console.log(a.name);}嚴格的空檢查
當strictNullChecks選項為false時,TypeScript實際上會忽略null和undefined。這可能會在運行時導致意外錯誤。
當strictNullChecks設置為true時,null和undefined有它們自己的類型,如果你將它們分配給一個期望具體值(例如,字符串)的變量,則會得到一個類型錯誤。
letwhoSangThis:string=getSong();constsingles=[{song:'touchofgrey',artist:'gratefuldead'},{song:'paintitblack',artist:'rollingstones'},];constsingle=singles.find((s)=>s.song===whoSangThis);console.log(single.artist);
singles.find並不能保證它一定能找到這首歌 — 但是我們已經編寫了下面的代碼,好像它肯定能找到一樣。
通過將strictNullChecks設置為true,TypeScript將拋出一個錯誤,因為在嘗試使用它之前,我們沒有保證single一定存在:
constgetSong=()=>{return'song';};letwhoSangThis:string=getSong();constsingles=[{song:'touchofgrey',artist:'gratefuldead'},{song:'paintitblack',artist:'rollingstones'},];constsingle=singles.find((s)=>s.song===whoSangThis);console.log(single.artist);//ERROR:Objectispossibly'undefined'.
TypeScript基本上是告訴我們在使用single之前要確保它存在。我們需要先檢查它是否為null或undefined:
if(single){console.log(single.artist);//rollingstones}TypeScript 中的類型收窄
在TypeScript中,變量可以從不太精確的類型轉移到更精確的類型,這個過程稱為類型收窄。
下面是一個簡單的例子,展示了當我們使用帶有typeof的if語句時,TypeScript如何將不太特定的string | number縮小到更特定的類型:
functionaddAnother(val:string|number){if(typeofval==='string'){//ts將val視為一個字符串returnval.concat(''+val);}//ts知道val在這裡是一個數字returnval+val;}console.log(addAnother('哈哈'));//哈哈哈哈console.log(addAnother(17));//34
另一個例子:下面,我們定義了一個名為allVehicles的聯合類型,它可以是Plane或Train類型。
interfaceVehicle{topSpeed:number;}interfaceTrainextendsVehicle{carriages:number;}interfacePlaneextendsVehicle{wingSpan:number;}typePlaneOrTrain=Plane|Train;functiongetSpeedRatio(v:PlaneOrTrain){console.log(v.carriages);//ERROR:'carriages'doesn'texistontype'Plane'}
由於getSpeedRatio函數處理了多種類型,我們需要一種方法來區分v是Plane還是Train。我們可以通過給這兩種類型一個共同的區別屬性來做到這一點,它帶有一個字符串值:
interfaceTrainextendsVehicle{type:'Train';carriages:number;}interfacePlaneextendsVehicle{type:'Plane';wingSpan:number;}typePlaneOrTrain=Plane|Train;
現在,TypeScript可以縮小 v 的類型:
functiongetSpeedRatio(v:PlaneOrTrain){if(v.type==='Train'){returnv.topSpeed/v.carriages;}//如果不是 Train,ts 知道它就是 Plane 了,聰明!returnv.topSpeed/v.wingSpan;}letbigTrain:Train={type:'Train',topSpeed:100,carriages:20,};console.log(getSpeedRatio(bigTrain));//5
另外,我們還可以通過實現一個類型保護來解決這個問題,可以看看這篇文章:什麼是鴨子🦆類型?
TypeScript & React
TypeScript 完全支持 React 和 JSX。這意味着我們可以將 TypeScript 與三個最常見的 React 框架一起使用:
如果你需要一個更自定義的React-TypeScript配置,你可以字節配置Webpack和tsconfig.json。但是大多數情況下,一個框架就可以完成這項工作。
例如,要用TypeScript設置create-react-app,只需運行:
npxcreate-react-appmy-app--templatetypescript#oryarncreatereact-appmy-app--templatetypescript
在src文件夾中,我們現在可以創建帶有.ts(普通TypeScript文件)或.tsx(帶有React的TypeScript文件)擴展名的文件,並使用TypeScript編寫我們的組件。然後將其編譯成public文件夾中的JavaScript。
React props & TypeScript
Person是一個React組件,它接受一個props對象,其中name應該是一個字符串,age是一個數字。
//src/components/Person.tsximportReactfrom'react';constPerson:React.FC<{name:string;age:number;}>=({name,age})=>{return(<div><div>{name}</div><div>{age}</div></div>);};exportdefaultPerson;
一般我們更喜歡用interface定義props:
interfaceProps{name:string;age:number;}constPerson:React.FC<Props>=({name,age})=>{return(<div><div>{name}</div><div>{age}</div></div>);};
然後我們嘗試將組件導入到App.tsx,如果我們沒有提供必要的props,TypeScript會報錯。
importReactfrom'react';importPersonfrom'./components/Person';constApp:React.FC=()=>{return(<div><Personname='ConardLi'age={17}/></div>);};exportdefaultApp;React hooks & TypeScriptuseState()
我們可以用尖括號來聲明狀態變量的類型。如果我們省略了尖括號,TypeScript會默認推斷cash是一個數字。因此,如果想讓它也為空,我們必須指定:
constPerson:React.FC<Props>=({name,age})=>{const[cash,setCash]=useState<number|null>(1);setCash(null);return(<div><div>{name}</div><div>{age}</div></div>);};useRef()
useRef返回一個可變對象,該對象在組件的生命周期內都是持久的。我們可以告訴TypeScriptref對象應該指向什麼:
constPerson:React.FC=()=>{//Initialise.currentpropertytonullconstinputRef=useRef<HTMLInputElement>(null);return(<div><inputtype='text'ref={inputRef}/></div>);};參考
好了,這篇文章我們學習了一些Typescript的必備基礎,有了這些知識你已經可以應付大部分TS的應用場景了,後續我會出一些 TS 的高級技巧相關的文章,敬請期待吧 ~

