TypeScript 原来可以这么香?!
先問一個(gè)問題,JavaScript有幾種數(shù)據(jù)類型?
number、string、boolean、null、undefined、symbol、bigint、object
其中 bigint 是 ES2020 新增的數(shù)據(jù)類型,而早在 TS3.2 時(shí)便成為 TS 的標(biāo)準(zhǔn),其實(shí)還有好多 ES+ 標(biāo)準(zhǔn)是 TS 率先提出的,可見 TS 在很多方面是走在了 ES 前列。
TypeScript又新增了多少種數(shù)據(jù)類型?
any、unknown、enum、void、never、tuple...
其實(shí) TypeScript 更重要的是通過 interface 和 type 賦予了用戶自定義數(shù)據(jù)類型的能力,使數(shù)據(jù)在流轉(zhuǎn)的過程中始終能輕易被用戶掌握。
Bug 絞殺者
TypeScript 可以讓我們的程序?qū)懙母?#xff0c;并且更好維護(hù),豐富的代碼的提示功能也能夠提高我們的開發(fā)效率以及降低協(xié)作成本,并幫助我們?cè)诔绦虻木幾g階段檢查出許多因?yàn)?strong>類型的原因?qū)е碌牡图?jí)錯(cuò)誤。
那么我們先來看下 JavaScript 項(xiàng)目最常見的十大錯(cuò)誤:
這些低級(jí)的錯(cuò)誤是不是耳熟能詳?為了解決這些問題,占用了我們大量的 debug 和 google 的時(shí)間。程序員最煩的兩件事:一件是自己寫完代碼還要寫注釋文檔,一件是別人的代碼沒留下任何注釋文檔。
在我們的日常開發(fā)中,很常見的是調(diào)用或者修改別人寫的函數(shù)。但是如果別人的代碼沒留下任何注釋的話,我們就要硬著頭皮去看里面的邏輯。
假如我們優(yōu)化了一個(gè)底層類庫(kù)的參數(shù)類型,而不知道有多少處引用,在提交代碼前,是不是內(nèi)心在打鼓,心里沒底呢?畢竟我們都不想被殺了祭天吧...
筆者前不久就因?yàn)橐粋€(gè)很小的問題 debug 了很長(zhǎng)時(shí)間,我們來看一下,因內(nèi)部代碼所以經(jīng)過脫敏處理。
因?yàn)?JavaScript 太靈活了,它允許你對(duì)參數(shù)做任何操作,所以之前同學(xué)寫的時(shí)候直接將 userId 掛載到入?yún)?query 上面,后面用到處解構(gòu)也就有了相應(yīng)的 userId 屬性。但是筆者對(duì)這部分業(yè)務(wù)不熟悉,以為入?yún)?query 不會(huì)改變,所以 debug 了好久。
而假如我們使用的是 TypeScript ,我們會(huì)定義好入?yún)?query 的結(jié)構(gòu),為 query 添加 userId 屬性的時(shí)候就能有報(bào)錯(cuò)提示,可以規(guī)避我們以前的寫法,換成更健壯性的寫法。
另外 TypeScript 還能實(shí)現(xiàn)自文檔化,使后面維護(hù)的同學(xué)也能輕松的接手。
我們可以通過/** */來注釋 TypeScript 的類型,當(dāng)我們?cè)谑褂孟嚓P(guān)類型的時(shí)候就會(huì)有注釋的提示,這個(gè)技巧可以幫助我們節(jié)約翻文檔或者跳頁(yè)看注釋的時(shí)間,在團(tuán)隊(duì)內(nèi)合理的推廣使用,能夠極大的提高我們的開發(fā)效率。
優(yōu)化程序性能
合理的使用 TypeScript ,再憑借 V8 引擎可以幫助我們極大的優(yōu)化程序的性能。
在 V8 引擎下,引入了 TurboFan 編譯器,它會(huì)在特定的情況下進(jìn)行優(yōu)化,將代碼編譯成執(zhí)行效率更高的 Machine Code,這個(gè)編譯器雖然不是 JavaScript 必須的,但是卻能夠極大的提高代碼執(zhí)行效能。
我們知道, JavaScript 代碼首先會(huì)解析為抽象語(yǔ)法樹(AST),然后會(huì)通過解釋器或者編譯器轉(zhuǎn)化為 Bytecode 或者 Machine Code。其中 Ignition 負(fù)責(zé)將 AST 轉(zhuǎn)化為 Bytecode,TurboFan 負(fù)責(zé)編譯出優(yōu)化后的 Machine Code,并且 Machine Code 在執(zhí)行效率上優(yōu)于 Bytecode。
那么問題來了,什么情況下的代碼會(huì)編譯為 Machine Code 呢?JavaScript 是一門動(dòng)態(tài)類型語(yǔ)言,并且有一大堆的隱式類型轉(zhuǎn)換規(guī)則,比如數(shù)字相加、字符串相加、對(duì)象和字符串相加等等。這樣的情況也就勢(shì)必導(dǎo)致了內(nèi)部要增加很多的判斷邏輯,降低運(yùn)行時(shí)的效率。
function test (x) {return x + x; }test(1) test(2) test(3) test(4)對(duì)于上面這段代碼來說,如果一個(gè)函數(shù)被多次調(diào)用并且參數(shù)一直傳入 number 類型,那么 V8 引擎就會(huì)認(rèn)為該段代碼可以編譯為 Machine Code,因?yàn)槲覀児潭祟愋?#xff0c;不需要再執(zhí)行很多判斷邏輯了。
但是一旦我們傳入的參數(shù)類型改變,那么 Machine Code 就會(huì)被 DeOptimized 為 Bytecode,這樣就會(huì)造成性能上的損耗。所以如果我們希望代碼能盡可能多的編譯為 Machine Code 并且 DeOpimized 的次數(shù)減少,那么我們就應(yīng)該盡可能的保證傳入的類型一致。
但是你可能還有一個(gè)疑問,優(yōu)化前后的性能提升到底是怎么樣的呢?有什么數(shù)據(jù)支撐么?
const v8 = require('v8-natives'); const { performance, PerformanceObserver } = require('perf_hooks')function test(x, y) {return x + y }const obs = new PerformanceObserver((list, observer) => {console.log(list.getEntries())observer.disconnect() }) obs.observe({ entryTypes: ['measure'], buffered: true })performance.mark('start')let number = 10000000// 不優(yōu)化代碼 v8.neverOptimizeFunction(test)while (number--) {test(1, 2) }performance.mark('end') performance.measure('test', 'start', 'end')我們接下來使用 performance 這個(gè) API 測(cè)量一下代碼的執(zhí)行時(shí)間,這個(gè) API 經(jīng)常用于一些性能測(cè)試,還可以用來測(cè)量各種網(wǎng)絡(luò)連接中的時(shí)間消耗,并且也可以在瀏覽器中使用。
我們實(shí)際運(yùn)行一下代碼發(fā)現(xiàn),經(jīng)過優(yōu)化后的代碼執(zhí)行時(shí)間只需要 10ms,但是不優(yōu)化的代碼卻是前者的十二倍,高達(dá)了 124ms。
在這個(gè)案例中,我們能夠看到 V8 的性能優(yōu)化十分強(qiáng)大,只需要我們符合一定規(guī)則書寫代碼,引擎底層就能幫助我們自動(dòng)優(yōu)化代碼。
那么為了讓 V8 優(yōu)化代碼,我們要盡可能的保證傳入?yún)?shù)的類型一致,而這,也正是 TypeScript 帶給我們的好處之一,借助 TypeScript,可以強(qiáng)迫我們思考定義好每一處的變量類型,讓每一處的變量類型都做到最小可控。使 V8 可以自動(dòng)將我們的代碼優(yōu)化成 Machine Code。
所以我們可以設(shè)想一下,未來怎么樣憑借 TypeScript,讓 V8 進(jìn)一步得到優(yōu)化。
1、使用 TypeScript 編程,遵循嚴(yán)格的類型化編程規(guī)則,摒棄 AnyScript。
2、構(gòu)建的時(shí)候?qū)?TypeScript 直接編譯為 Bytecode,而不是 JavaScript 文件,這樣運(yùn)行的時(shí)候就省去了 Parse 以及生成 Bytecode 的過程。
3、運(yùn)行的時(shí)候,需要先將 Bytecode 編譯為對(duì)應(yīng) CPU 的匯編代碼。
這樣由于采用了類型化的編程方式,有利于編譯器優(yōu)化所生成的匯編代碼,省去了很多額外的操作。
未來的潮流
很多的前端底層庫(kù)都在從 JavaScript 向 TypeScript 遷移,像我們熟悉的 Angular 和 Vue3 已經(jīng)全面用 TypeScript 重構(gòu)代碼,在 ECMAScript 標(biāo)準(zhǔn)推出靜態(tài)類型檢查之前,TypeScript 是解決當(dāng)下問題的最佳實(shí)踐。
stackoverflow 統(tǒng)計(jì)的2020年最受開發(fā)者喜歡的語(yǔ)言,TypeScript 已排到第二名。
但是很多同學(xué)一開始可能不太喜歡使用 TypeScript,人們?cè)诮佑|一個(gè)新事物的時(shí)候往往會(huì)出現(xiàn)本能的抗拒,因?yàn)椴惶_定這個(gè)新的事物能夠帶給我們什么開發(fā)體驗(yàn)的提升。
可能有些同學(xué)抱著試試看的態(tài)度去嘗試使用了一下,但卻發(fā)現(xiàn)這個(gè)東西巨難用,常常代碼死活編譯不過去,各種類型不匹配。一些底層類庫(kù)的不完善,沒有很好的支持 TypeScript 也會(huì)增加我們開發(fā)的上手難度。
這也難怪,因?yàn)?JavaScript 是一門動(dòng)態(tài)弱類型語(yǔ)言,對(duì)變量的類型非常寬容,而且不會(huì)在這些變量和它們的調(diào)用者之間建立結(jié)構(gòu)化的契約,JavaScript 帶給我們極大的靈活性。
一想到以前快樂的時(shí)光,可能有的同學(xué)直接棄療,重新回到 JavaScript 的懷抱;可能有的同學(xué)轉(zhuǎn)而去寫 AnyScript,遇事不決上 any,但這是非常差的編程習(xí)慣!假如我們寫 AnyScript,我們就失去了 TypeScript 的意義,為此我們還要多寫好多的冗余代碼,影響開發(fā)效率。
其實(shí)使用 TypeScript 進(jìn)行開發(fā),理論上我們99.99%的情況都不應(yīng)該使用 any,當(dāng)我們想要使用 any 的時(shí)候,就多去思考,查閱資料,仔細(xì)推敲,只要我們能夠不斷忍住不寫 any,多寫 TypeScript,寫到如臂指使,后面你會(huì)發(fā)現(xiàn)自己的開發(fā)效率在不斷提升。
實(shí)用小技巧
1. 巧用 keyof
假如我們要實(shí)現(xiàn)一個(gè) getValue 函數(shù),作用是根據(jù)傳入的 object 和 key 獲取 object 上 key 所對(duì)應(yīng)的屬性值 value,一開始的 JavaScript 版本可能是這樣的。
const data = {a: 3,hello: 'world' } function getValue(o, name) {return o[name] }可以看到這就是 JavaScript 的弊端,我們對(duì)傳入的 o 和 name 都沒有約束,對(duì)這一切都是未知的,人的恐懼就源自對(duì)未知事物的不確定性。尤其如果這是一個(gè)底層類庫(kù),我們不知道有多少人去調(diào)用,那這個(gè)風(fēng)險(xiǎn)就更加增大了,所以我們可能用 TypeScript 這樣去改造它。
function getValue(o: any, name: string) {return o[name] }但這樣寫貌似也有很多問題,函數(shù)返回值的類型變成了 any,失去了 TypeScript 類型校驗(yàn)的功能,讓類型重新變的不可控,name 類型固然是 string,但還是太寬泛了,實(shí)際上,name 的值只能是o的鍵的并集,而且如果我們將 name 拼寫錯(cuò)誤,TypeScript 也不會(huì)幫我們報(bào)錯(cuò)提示。這個(gè)時(shí)候我們可以使用 keyof 結(jié)合泛型來加強(qiáng) getValue 函數(shù)的類型功能。
function getValue<T extends object, K extends keyof T>(o: T, name: K): T[K] {return o[name] }2. 接口智能提示
interface Seal {name: string;url: string; } interface API {"/user": { name: string; age: number; phone: string };"/seals": { seal: Seal[] }; } const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {return fetch(url).then((res) => res.json()); };借助泛型以及泛型約束,我們可以實(shí)現(xiàn)智能提示的功能,不光接口名可以智能提示,接口返回也可以智能提示。當(dāng)我們輸入 api 的時(shí)候,其會(huì)自動(dòng)將 API interface 下的所有 key 提示給我們,當(dāng)我們輸入某一個(gè) key 的時(shí)候,其會(huì)根據(jù) key 命中的 interface 定義的類型,然后給予類型提示。這在統(tǒng)一接口管理方面有很大的用處,可以幫助我們面向接口編程。
3. 巧用類型保護(hù)
interface User {name: string;age: number;occupation: string; } interface Admin {name: string;age: number;role: string; } export type Person = User | Admin; export const persons: Person[] = [{name: 'Max Mustermann',age: 25,occupation: 'Chimney sweep'},{name: 'Jane Doe',age: 32,role: 'Administrator'},{name: 'Kate Müller',age: 23,occupation: 'Astronaut'},{name: 'Bruce Willis',age: 64,role: 'World saver'} ]; export function logPerson(person: Person) {let additionalInformation: string;if (person.role) {additionalInformation = person.role;} else {additionalInformation = person.occupation;}console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`); } persons.forEach(logPerson);我們可以看到,當(dāng)我們定義了兩種 Person 類型:User 和 Admin,而在使用的時(shí)候是比較寬泛的 Person,那我們就不能直接使用 User 或者 Admin 的特有屬性 role 或者 occupation。因?yàn)?TypeScript 沒有足夠的信息確定 Person 究竟是 User 還是 Admin。
一種方法是使用類型斷言,顯示的告訴 TypeScript,person 就是 Admin 類型或者就是 User 類型,但是這樣做一方面不夠優(yōu)雅,要在每一處都加上斷言;另一方面濫用斷言也會(huì)讓我們的代碼變得不可控,不能讓 TypeScript 幫助我們進(jìn)行合理的類型推斷。像雙重?cái)嘌钥梢砸?guī)避掉 TypeScript 的類型檢查機(jī)制也是與 any 一樣,要盡可能去避免的。
正確的做法是使用類型收縮,比如使用 is,in,typeof,instanceof 等,使得 TypeScript 能夠 get 到當(dāng)前的類型,假如 person 上面有 role 屬性,TypeScript 就可以推斷出 person 就是 Admin 類型,創(chuàng)建類型保護(hù)區(qū)塊,在當(dāng)前的代碼塊按照 Admin 類型處理,代碼也簡(jiǎn)潔了很多。
同樣是這個(gè)例子,我們?cè)俑脑煲幌?#xff0c;通過兩個(gè)函數(shù)來判斷 person 的具體類型是 Admin 還是 User。但是很不幸,TypeScript 依然不能很智能的知道 person 在第一個(gè)代碼塊里是 Admin 類型,在第二個(gè)代碼塊里是 User 類型。
這個(gè)時(shí)候我們就要改造一下 isAdmin 和 isUser 的函數(shù)返回,創(chuàng)建用戶自定義的類型保護(hù)函數(shù),顯式的告訴 TypeScript,函數(shù)返回為 true 時(shí),指定 person 的類型確定為 Admin 或者 User,這樣 TypeScript 就知道 person 的確定類型了,這就是類型位詞。
4.常用的高級(jí)類型
這其實(shí)涉及到了類型編程到概念,簡(jiǎn)而言之,我們平時(shí)寫代碼是對(duì)值進(jìn)行編程,而類型編程是對(duì)類型進(jìn)行編程,可以利用 keyof 對(duì)屬性做一些擴(kuò)展,省的我們要重新定義一下接口,造成很多冗余代碼。
這些高級(jí)類型在日常編程中有非常廣泛的使用,尤其 Partial 可以將所有的屬性變成可選的,如在我們?nèi)粘5乃阉鬟壿?#xff0c;我們可以根據(jù)單一條件搜索,也可以根據(jù)組合條件搜索。Omit 可以幫助我們復(fù)用一個(gè)類型,但是又不需要此類型內(nèi)的全部屬性,當(dāng)父組件通過 props 向下傳遞數(shù)據(jù)的時(shí)候,可以剔除一些無(wú)用的類型。
Record 也是一個(gè)比較常用的高級(jí)類型,可以幫助我們從 Union 類型中創(chuàng)建新類型,Union 類型中的值用作新類型的屬性。當(dāng)我們拼寫錯(cuò)誤,或者漏寫一些屬性,或者加入了沒有預(yù)先定義的屬性進(jìn)去,TypeScript 都可以給我們很友好的報(bào)錯(cuò)提示。
type Partial<T> = {[P in keyof T]?: T[P]; }; type Required<T> = {[P in keyof T]-?: T[P]; }; type Pick<T, K extends keyof T> = {[P in K]: T[P]; }; type Exclude<T, U> = T extends U ? never : T; // 相當(dāng)于: type A = 'a' type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'> type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; type Record<K extends keyof any, T> = {[P in K]: T; };interface User {id: number;age: number;name: string; }; // 相當(dāng)于: type PartialUser = { id?: number; age?: number; name?: string; } type PartialUser = Partial<User> // 相當(dāng)于: type PickUser = { id: number; age: number; } type PickUser = Pick<User, "id" | "age"> // 相當(dāng)于: type OmitUser = { age: number; name: string; } type OmitUser = Omit<User, "id">type AnimalType = 'cat' | 'dog' | 'frog'; interface AnimalDescription { name: string, icon: string } const AnimalMap: Record<AnimalType, AnimalDescription> = {cat: { name: '貓', icon: ' '},dog: { name: '狗', icon: ' ' },forg: { name: '蛙', icon: ' ' }, // Hey! };5.巧用類型約束
在 .tsx 文件中,泛型可能會(huì)被當(dāng)作 jsx 標(biāo)簽
加 extends 可破
const toArray = <T extends {}>(element: T) => [element]; // No errors.TypeScript 還可以給一些缺乏類型定義的第三方庫(kù)定義類型,找到一些沒有 d.ts 聲明的開源庫(kù),為開源社區(qū)貢獻(xiàn)聲明文件。
學(xué)習(xí)參考
https://www.typescriptlang.org/docs/handbook/release-notes/overview.html 官方各個(gè)版本文檔
https://github.com/microsoft/TypeScript/projects/9 官方討論
https://github.com/microsoft/vscode VS Code 是 TypeScript 編寫的,毫無(wú)疑問也是學(xué)習(xí)的好地方
https://basarat.gitbook.io/typescript/getting-started TypeScript Deep Dive
https://github.com/typescript-exercises/typescript-exercises TypeScript 優(yōu)秀的練習(xí)題
總結(jié)
以上是生活随笔為你收集整理的TypeScript 原来可以这么香?!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dea分析的matlab实现,利用MAT
- 下一篇: 带你了解APP开发的具体流程