十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊(duì)
量身定制 + 運(yùn)營(yíng)維護(hù)+專業(yè)推廣+無(wú)憂售后,網(wǎng)站問題一站解決
這篇文章主要介紹“捕獲更多Bug的TypeScript建議有哪些”,在日常操作中,相信很多人在捕獲更多Bug的TypeScript建議有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”捕獲更多Bug的TypeScript建議有哪些”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
創(chuàng)新互聯(lián)公司是一家專注網(wǎng)站建設(shè)、網(wǎng)絡(luò)營(yíng)銷策劃、小程序定制開發(fā)、電子商務(wù)建設(shè)、網(wǎng)絡(luò)推廣、移動(dòng)互聯(lián)開發(fā)、研究、服務(wù)為一體的技術(shù)型公司。公司成立十載以來(lái),已經(jīng)為上千家混凝土泵車各業(yè)的企業(yè)公司提供互聯(lián)網(wǎng)服務(wù)?,F(xiàn)在,服務(wù)的上千家客戶與我們一路同行,見證我們的成長(zhǎng);未來(lái),我們一起分享成功的喜悅。
1. 對(duì)TypeScript提供運(yùn)行時(shí)檢查的思考
有一個(gè)對(duì)TypeScript常見的誤解是:一個(gè)變量只要標(biāo)注了類型,那么它總是會(huì)檢查自己的數(shù)據(jù)類型是否與我們的預(yù)期一致。
與該誤解相呼應(yīng)的想法會(huì)認(rèn)為:對(duì)一個(gè)從后端返回的對(duì)象進(jìn)行類型標(biāo)注可以在代碼運(yùn)行時(shí)執(zhí)行檢查來(lái)確保對(duì)象類型的正確性。
然而這個(gè)想法是錯(cuò)誤的!因?yàn)門ypeScript最終是被編譯成JavaScript代碼,并且瀏覽器中運(yùn)行的也是JavaScript。此時(shí)(譯者注:運(yùn)行時(shí))所有的類型信息都丟失了,所以TypeScript無(wú)法自動(dòng)驗(yàn)證類型。
理解這一點(diǎn)的一個(gè)好方法是查看編譯后的代碼:
interface Person { name: string; age: number; } function fetchFromBackend(): Promise{ return fetch('http://example.com') .then((res) => res.json()) } // 編譯后 function fetchFromBackend() { return fetch('http://example.com') .then(function(res) { return res.json(); }) }
可以看到接口定義在編譯后已經(jīng)完全消失了,而且這里也不會(huì)有任何驗(yàn)證性的代碼。
不過你最好可以自己去執(zhí)行運(yùn)行時(shí)校驗(yàn),許多庫(kù)(譯者注:io-ts)能幫你做到這點(diǎn)。不過,請(qǐng)記住,這一定會(huì)帶來(lái)性能開銷。
* 考慮對(duì)所有外部提供的對(duì)象執(zhí)行運(yùn)行時(shí)檢查(例如從后端獲取的對(duì)象,JSON反序列化的對(duì)象等)
2. 不要將類型定義為any
使用TypeScript時(shí),可以將變量或函數(shù)參數(shù)的類型聲明為any,但是這樣做也意味著該變量脫離了類型安全保障。
不過聲明為any類型也會(huì)有好處,在某種場(chǎng)景下很有幫助(例如將類型逐步添加到現(xiàn)有的JavaScript代碼庫(kù)中,譯者注:一般是將代碼庫(kù)從js升級(jí)到ts時(shí))。但是它也像一個(gè)逃生艙口,會(huì)大大降低代碼的類型安全性。
當(dāng)類型安全涵蓋盡可能多的代碼時(shí),它是最有效的。否則,安全網(wǎng)中會(huì)存在漏洞,漏洞可能會(huì)通過漏洞傳播。例如:如果函數(shù)返回any,則使用其返回值的所有表達(dá)式類型也將變成any。
所以你應(yīng)該盡量避免使用any類型。幸運(yùn)的是,TypeScript3.0引入了類型安全的替代方案——unknown??梢詫⑷魏沃蒂x給unknown類型的變量,但是不能將unknown類型的變量的值賦給任何變量(這點(diǎn)不同于any)。
如果你的函數(shù)返回的是unknown類型的值,則調(diào)用方需要執(zhí)行檢查(使用類型保護(hù)),或至少將值顯式轉(zhuǎn)換為某個(gè)特定類型。(譯者注:如果對(duì)這段不理解,可以參考下這篇文章,unknown 類型 中的示例部分)
let foo: any; // anything can be assigned to foo foo = 'abc'; // foo can be assigned to anything const x: number = foo; let bar: unknown; // anything can be assigned to bar bar = 'abc'; // COMPILE ERROR! Type 'unknown' is not assignable to type 'number'. const y: number = bar;
使用unknown類型有時(shí)會(huì)有些麻煩,但是這也會(huì)讓代碼更易于理解,并且讓你在開發(fā)時(shí)更加注意。
另外,你需要開啟noImplicitAny,每當(dāng)編譯器推斷某個(gè)值的類型為any時(shí)就會(huì)拋出錯(cuò)誤。換句話說(shuō),它讓你顯式的標(biāo)注出所有會(huì)出現(xiàn)any的場(chǎng)景。
盡管最終目標(biāo)還是消除有any的情況,但明確申明any仍然是有益的:例如在code review時(shí)可以更容易捕獲他們。
* 不要使用any類型并開啟noImplicitAny
3. 開啟strictNullChecks
你已經(jīng)見過多少次這樣的報(bào)錯(cuò)信息了?
TypeError: undefined is not an object
我打賭有很多次了,JavaScript(甚至是軟件編程)中最常見的bug來(lái)源之一就是忘記處理空值。
在JavaScript中用null或undefined來(lái)表示空值。開發(fā)者們經(jīng)常樂觀的認(rèn)為給定的變量不會(huì)是空的,于是就忘記處理空值的情況。
function printName(person: Person) { console.log(person.name.toUpperCase()); } // RUNTIME ERROR! TypeError: undefined is not an object // (evaluating 'person.name') printName(undefined);
通過開啟strictNullChecks,編譯器會(huì)迫使你去做相關(guān)的檢查,這對(duì)防止出現(xiàn)這種常見問題起到了重要的作用。
默認(rèn)情況下,typescript的每個(gè)類型都包含null和undefined這兩個(gè)值。也就是說(shuō),null和undefined可以被賦值給任意類型的任何變量。
而開啟strictNullChecks會(huì)更改該行為。由于無(wú)法將undefined作為Person類型的參數(shù)傳遞,因此下方的代碼會(huì)在編譯時(shí)報(bào)錯(cuò)。
// COMPILE ERROR! // Argument of type 'undefined' is not assignable to parameter of type 'Person'. printName(undefined);
那如果你確實(shí)就想將undefined傳遞給printName怎么辦?那你可以調(diào)整類型簽名,但是仍然會(huì)要求你處理undefined的情況。
function printName(person: Person | undefined) { // COMPILE ERROR! // Object is possibly 'undefined'. console.log(person.name.toUpperCase()); }
你可以通過確保person是被定義的來(lái)修復(fù)這個(gè)錯(cuò)誤:
function printName(person: Person | undefined) { if (person) { console.log(person.name.toUpperCase()); } }
不幸的是,strictNullChecks默認(rèn)是不開啟的,我們需要在tsconfig.json中進(jìn)行配置。
另外,strictNullChecks是更通用的嚴(yán)格模式的一部分,可以通過strict標(biāo)志啟用它。你絕對(duì)應(yīng)該這樣做!因?yàn)榫幾g器的設(shè)置越嚴(yán)格,你就可以盡早發(fā)現(xiàn)更多bug。
* 始終開啟strictNullChecks
4. 開啟strictPropertyInitialization
strictPropertyInitialization是屬于嚴(yán)格模式標(biāo)志集的另一個(gè)標(biāo)志。尤其在使用Class時(shí)開啟strictPropertyInitialization很重要,它其實(shí)有點(diǎn)像是對(duì)strictNullChecks的擴(kuò)展。
如果不開啟strictPropertyInitialization的話,TS會(huì)允許以下的代碼:
class Person { name: string; sayHello() { // RUNTIME ERROR! console.log( `Hello from ${this.name.toUpperCase()}`); } }
這里有個(gè)很明顯的問題:this.name沒有被初始化,因此在運(yùn)行時(shí)調(diào)用sayHello就會(huì)報(bào)錯(cuò)。
造成這個(gè)錯(cuò)誤的根本原因是這個(gè)屬性沒有在構(gòu)造函數(shù)里或使用屬性初始化器賦值,所以它(至少在最初)是undefined,因此他的類型就會(huì)變成string | undefined。
開啟strictPropertyInitialization會(huì)提示以下錯(cuò)誤:
Property 'name' has no initializer and is not assigned in the constructor.
當(dāng)然,如果你在構(gòu)造函數(shù)里或使用屬性初始化器賦值了,這個(gè)錯(cuò)誤也就會(huì)消失。
* 始終開啟strictPropertyInitialization
5. 記得指定函數(shù)的返回類型
TypeScript使你可以高度依賴類型推斷,這意味著只要在TS能推斷類型的地方,你就不需要標(biāo)注類型。
然而這就像一把雙刃劍,一方面,它非常方便,并且減少了使用TypeScript的麻煩。而另一方面,有時(shí)推斷的類型可能會(huì)和你的預(yù)期不一致,從而降低了使用靜態(tài)類型提供的保障。
在下方的例子中,我們沒有注明返回類型,而是讓TypeScript來(lái)推斷函數(shù)的返回值。
interface Person { name: string; age: number; } function getName(person: Person | undefined) { if (person && person.name) { return person.name; } else if (!person) { return "no name"; } }
乍看之下,我們可能認(rèn)為我們的方法很安全,并且始終返回的是string類型,然而,當(dāng)我們明確聲明該函數(shù)的(預(yù)期)返回類型時(shí)就會(huì)發(fā)現(xiàn)報(bào)了一個(gè)錯(cuò)。
// COMPILE ERROR! // Function lacks ending return statement and return type does not include 'undefined'. function getName(person: Person | undefined): string { // ... }
順便說(shuō)一句,這個(gè)錯(cuò)誤只有當(dāng)你開啟了strictNullChecks才會(huì)被檢測(cè)出來(lái)。
上述錯(cuò)誤表明getName函數(shù)的返回值沒有覆蓋到一種情況:當(dāng)person不為空,但是person.name為空的情況。這種情況所有if條件都不等于true,所以會(huì)返回undefined。
因此,TypeScript推斷此函數(shù)的返回類型為string | underfined,而我們聲明的卻是string。(譯者注:所以主動(dòng)聲明函數(shù)返回值類型有助于幫我們提前捕捉一些不易察覺的bug)
* 始終標(biāo)注函數(shù)的返回值類型
6. 不要將隱式類型變量存儲(chǔ)到對(duì)象中
TypeScript的類型檢查有時(shí)很微妙。
通常,當(dāng)類型A至少具有和類型B相同的屬性,那么TypeScript就允許將類型A的對(duì)象賦值給類型B的變量。這意味著它可以包含其他屬性。
// 譯者舉例: type A = { name: string; age: number; }; type B = { name: string; }; let a: A = { name: 'John', age: 12, }; let b: B; // compile success b = a;
然而如果直接傳遞的是對(duì)象字面量,其行為是不同的。只有目標(biāo)類型包含相同的屬性時(shí),TypeScript才會(huì)允許它(傳遞)。此時(shí)不允許包含其他屬性。
interface Person { name: string; } function getName(person: Person): string | undefined { // ... } // ok getName({ name: 'John' }); // COMPILE ERROR // Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'Person'. getName({ name: 'John', age: 30 });
如果我們不是直接傳對(duì)象字面量,而是將對(duì)象存到常量里(再傳遞),這看起來(lái)沒有什么區(qū)別。然而這卻更改了類型檢查的行為:
const person = { name: 'John', age: 30 }; // OK getName(person);
傳遞額外的屬性可能會(huì)引起bug(例如當(dāng)你想合并兩個(gè)對(duì)象時(shí))。了解這個(gè)行為并且在可能的情況下,直接傳遞對(duì)象字面量。
* 請(qǐng)注意如何將對(duì)象傳遞給函數(shù)并且始終要考慮傳遞額外的屬性是否安全
7. 不要過度使用類型斷言
盡管TypeScript能對(duì)你的代碼進(jìn)行很多推斷,但有時(shí)候你會(huì)比TypeScript更了解某個(gè)值的詳細(xì)信息。這時(shí)你可以通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。
比如說(shuō)對(duì)一個(gè)從服務(wù)器請(qǐng)求回來(lái)的對(duì)象斷言,或者將一個(gè)子類型的對(duì)象斷言為父類型。
類型斷言需要保守使用。比如絕對(duì)不能在函數(shù)傳參類型不匹配時(shí)使用。
有一種更安全的使用類型斷言的方式:類型保護(hù)。類型保護(hù)是一個(gè)當(dāng)返回true時(shí)能斷言其參數(shù)類型的函數(shù)。它可以提供代碼運(yùn)行時(shí)的檢測(cè),讓我們對(duì)傳入的變量是否符合預(yù)期這點(diǎn)上更有信心。
下面的代碼中,我們需要使用類型斷言,因?yàn)門ypeScript不知道從后端返回的對(duì)象的類型。
interface Person { name: string; age: number; } declare const fetchFromBackend: (url: string) => Promise
我們可以通過使用類型保護(hù),提供一個(gè)簡(jiǎn)單的運(yùn)行時(shí)檢查來(lái)讓代碼更完善。我們假設(shè)一個(gè)對(duì)象只要擁有了name和age屬性那么它的類型就是Person。
const isPerson = (obj: Object): obj is Person => 'name' in obj && 'age' in obj; fetchFromBackend('/person/1').then((person) => { if(isPerson(person)) { // Type of `person` is `Person` here! displayPerson(person); } })
你可以發(fā)現(xiàn),多虧了類型保護(hù),在if語(yǔ)句中person的類型已經(jīng)可以被正確推斷了。
* 考慮使用類型保護(hù)來(lái)替代類型斷言
8. 不要對(duì)Partial類型使用擴(kuò)展運(yùn)算符
Partial是一個(gè)非常有用的類型,它的作用是將源類型的每個(gè)屬性都變成可選的。
Partial有個(gè)好的實(shí)際使用場(chǎng)景:當(dāng)你有一個(gè)表示配置或選項(xiàng)的對(duì)象類型,并且想要?jiǎng)?chuàng)建一個(gè)該配置對(duì)象的子集來(lái)覆寫它。
你可能會(huì)寫出如下的代碼:
interface Settings { a: string; b: number; } const defaultSettings: Settings = { /* ... */ }; function getSettings(overrides: Partial): Settings { return { ...defaultSettings, ...overrides }; }
這看起來(lái)還不錯(cuò),但實(shí)際上揭示了TypeScript的類型系統(tǒng)中的一個(gè)漏洞。
看下方的代碼,result的類型是Settings,然而result.a的值卻是undefined了。
const result = getSettings({ a: undefined, b: 2 });
由于擴(kuò)展Partial是一種常見的模式,并且TypeScript的目標(biāo)之一是在嚴(yán)格性和便利性之間取得平衡,所以可以說(shuō)是TypeScript本身的設(shè)計(jì)帶來(lái)了這種不一致性。但是,意識(shí)到該問題仍然非常重要。
* 除非你確定對(duì)象里不包含顯式的undefined,否則不要對(duì)Parital對(duì)象使用擴(kuò)展運(yùn)算符
9. 不要過于相信Record類型
這是TypeScript內(nèi)置類型定義中的一個(gè)微妙情況的另一個(gè)示例。
Record定義了一個(gè)對(duì)象類型,其中所有key具有相同的類型,所有value具有相同的類型。 這非常適合表示值的映射和字典。
換句話說(shuō),Record
從下方代碼你可以看出,通過訪問record對(duì)象的屬性返回的值的類型應(yīng)該和ValueType保持一致。然而你會(huì)發(fā)現(xiàn)這不是完全正確的,因?yàn)閍bc的值會(huì)是undefined。
const languages: Record= { 'c++': 'static', 'java': 'static', 'python': 'dynamic', }; const abc: string = languages['abc']; // undefined
這又是一個(gè)TypeScript選擇了便利性而不是嚴(yán)格性的例子。雖然大多數(shù)例子中這樣使用都是可以的,但是你仍然要小心些。
最簡(jiǎn)單的修復(fù)方式就是使Record的第二個(gè)參數(shù)可選:
const languages: Partial> = { 'c++': 'static', 'java': 'static', 'python': 'dynamic', }; const abc = languages['abc']; // abc is infer to string | underfined
* 除非你確保沒問題,否則可以始終保持Record的值類型參數(shù)(第二個(gè)參數(shù))可選
10. 不要允許出現(xiàn)不合格的類型聲明
在定義業(yè)務(wù)域?qū)ο蟮念愋蜁r(shí),通常會(huì)遇到類似以下的情況:
interface Customer { acquisitionDate: Date; type: CustomerType; firstName?: string; lastName?: string; socialSecurityNumber?: string; companyName?: string; companyTaxId?: number; }
這個(gè)對(duì)象包含很多可選的對(duì)象。其中一些對(duì)象是當(dāng)Customer表示人時(shí)(type === CustomerType.Individual)才有意義且必填,另外的則是當(dāng)Custormer表示公司時(shí)(type === CustomerType.Institution)必填。
問題在于Customer類型不能反映這一點(diǎn)! 換句話說(shuō),它允許屬性的某些非法組合(例如,lastName和companyName都未定義)
這確實(shí)是有問題的。 你要么執(zhí)行額外的檢查,要么使用類型斷言來(lái)消除基于type屬性值的某些字段的可選性。
幸運(yùn)的是,有一個(gè)更好的解決方案——辨析聯(lián)合類型。辨析聯(lián)合類型是在聯(lián)合類型的基礎(chǔ)上增加了一個(gè)功能:在運(yùn)行時(shí)可以區(qū)分不同的方案。
我們將Customer類型重寫為兩種類型:Individual和Institution的聯(lián)合,各自包含一些特定的字段,并且有一個(gè)共有字段:type,它的值是一個(gè)字符串。此字段允許運(yùn)行時(shí)檢查,并且TypeScript知道可以專門處理它。
interface Individual { kind: 'individual'; firstName: string; lastName: string; socialSecurityNumber: number; } interface Institution { kind: 'institutional'; companyName: string; companyTaxId: number; } type Customer = Individual | Institution;
辨析聯(lián)合類型真正酷的地方是TypeScript提供了內(nèi)置的類型保護(hù),可以讓你避免類型斷言。
function getCustomerName(customer: Customer) { if (customer.kind === 'individual') { // The type of `customer` id `Individual` return customer.lastName; } else { // The type of `customer` id `Institution` return customer.companyName; } }
* 當(dāng)遇到復(fù)雜的業(yè)務(wù)對(duì)象時(shí)盡量考慮使用辨析聯(lián)合類型。這可以幫你創(chuàng)建更貼合現(xiàn)實(shí)場(chǎng)景的類型
到此,關(guān)于“捕獲更多Bug的TypeScript建議有哪些”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!