DeepKit —— 賦予 TypeScript 更多可能性
本文為來(lái)自飛書(shū) aPaaS Growth 研發(fā) 團(tuán)隊(duì)成員的文章,已授權(quán) ELab 發(fā)布。
aPaaS Growth 團(tuán)隊(duì)專注在用戶可感知的、宏觀的 aPaaS 應(yīng)用的搭建流程,及租戶、應(yīng)用治理等產(chǎn)品路徑,致力于打造 aPaaS 平臺(tái)流暢的 “應(yīng)用交付” 流程和體驗(yàn),完善應(yīng)用構(gòu)建相關(guān)的生態(tài),加強(qiáng)應(yīng)用搭建的便捷性和可靠性,提升應(yīng)用的整體性能,從而助力 aPaaS 的用戶增長(zhǎng),與基礎(chǔ)團(tuán)隊(duì)一起推進(jìn) aPaaS 在企業(yè)內(nèi)外部的落地與提效。
?背景?
之前在技術(shù)需求中曾調(diào)研了基于 TypeScript 的數(shù)據(jù)校驗(yàn)方案,其中調(diào)研了一個(gè)叫 Deepkit 的第三方庫(kù),可以將 TypeScript 的類型信息保留到運(yùn)行時(shí)進(jìn)行消費(fèi)。
?TypeScript 帶來(lái)的?
傳統(tǒng)開(kāi)發(fā)上,Javascript 基本沒(méi)有提供任何類型保護(hù),所有的類型錯(cuò)誤都需要在運(yùn)行時(shí)才能發(fā)現(xiàn),而TypeScript 為開(kāi)發(fā)者提供了一套靜態(tài)類型檢查的方案,它提倡開(kāi)發(fā)者在源碼中主動(dòng)聲明類型信息,并與對(duì)應(yīng)的變量和操作相匹配,并在編譯階段進(jìn)行檢查,類型相關(guān)的錯(cuò)誤在編譯時(shí)就暴露出來(lái),一方面使代碼更規(guī)范了,一方面也極大程度地規(guī)避了許多代碼錯(cuò)誤,提高了代碼的健壯性。
TypeScirpt 擁有完備的類型系統(tǒng)。但很可惜,它在這方面的能力在運(yùn)行時(shí)幾乎完全不存在。TypeScript Compiler在編譯源碼時(shí)會(huì)刪除類型信息,不對(duì)運(yùn)行時(shí)造成任何開(kāi)銷。
但其實(shí)在許多場(chǎng)景下,運(yùn)行時(shí)的類型信息都是極具價(jià)值的!
?為什么需要運(yùn)行時(shí)類型?
為什么我們需要運(yùn)行時(shí)的類型信息呢?讓我們看看下面兩個(gè)場(chǎng)景
數(shù)據(jù)校驗(yàn)
數(shù)據(jù)校驗(yàn)并不是局限于傳統(tǒng)前端所關(guān)注的表單校驗(yàn),需要數(shù)據(jù)校驗(yàn)的場(chǎng)景數(shù)不勝數(shù),比如:
在編寫(xiě)服務(wù)的時(shí)候,若我們需要實(shí)現(xiàn)一個(gè)接口。對(duì)于我們來(lái)說(shuō),傳入的參數(shù)是未知的,我們永遠(yuǎn)不知道業(yè)務(wù)方會(huì)給我傳來(lái)什么奇奇怪怪的參數(shù)。如果我們不對(duì)參數(shù)進(jìn)行校驗(yàn)的話,后面的代碼邏輯隨時(shí)可能崩潰。而參數(shù)校驗(yàn)自然就需要在運(yùn)行時(shí)消費(fèi)參數(shù)的類型定義信息。
數(shù)據(jù)庫(kù),一張表中所有字段的類型都是有嚴(yán)格定義的,所以在數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)時(shí),需要校驗(yàn)寫(xiě)入的數(shù)據(jù)是否符合字段的類型定義,這也需要運(yùn)行時(shí)的類型信息。
序列化與反序列化
序列化是將數(shù)據(jù)類型轉(zhuǎn)換為適合傳輸或存儲(chǔ)的格式的過(guò)程。反序列化是撤消此操作的過(guò)程,這個(gè)過(guò)程需要保證是無(wú)損的。對(duì)于前端開(kāi)發(fā)者來(lái)說(shuō),接觸的最多的應(yīng)該就是 JSON.parse()? 和 JSON.stringify() 這兩個(gè)方法。在簡(jiǎn)單場(chǎng)景下,用這兩個(gè)方法做序列化和反序列化可能沒(méi)有問(wèn)題,但是在復(fù)雜場(chǎng)景中就不一定了,因?yàn)檫@兩個(gè)方法并不能保證數(shù)據(jù)是無(wú)損的。
例如下面這個(gè)場(chǎng)景
對(duì)于日期類型的數(shù)據(jù),先用 JSON.stringify(date) 將其序列化成了適合傳輸?shù)母袷剑儆肑SON.parse(dateString) 反序列化,發(fā)現(xiàn)日期這個(gè)類型在過(guò)程中已經(jīng)丟失,最后反序列化的結(jié)果為一個(gè)字符串,這顯然是不符合預(yù)期的。因此,在序列化和反序列化的過(guò)程中,類型信息也十分重要。
而 DeepKit 使將 TypeScript 類型保留到運(yùn)行時(shí)成為現(xiàn)實(shí)。
?快速開(kāi)始?
官方文檔站:https://deepkit.io/
前置
使用 DeepKit 需要安裝兩個(gè)包:
- @deepkit/type:提供運(yùn)行時(shí)可以使用的方法
- @deepkit/type-compiler:類型編譯器,介入TypeScript 編譯流程,保留類型信息。可以放在package.json? 的devDependencies中,因?yàn)檫@個(gè)類型編譯器只需要編譯階段使用。
然后需要在 tsconfig.json? 中配置 "reflection": true? 。如果需要使用裝飾器,還需要加入"experimentalDecorators": true 參數(shù)
類型信息
DeepKit 定義了兩種用于描述運(yùn)行時(shí)的類型信息的數(shù)據(jù)結(jié)構(gòu),分別是類型對(duì)象和反射類。
類型對(duì)象
使用 typeOf 方法可以快速獲取某個(gè)類型對(duì)應(yīng)的類型對(duì)象。
從上面的例子中,我們可以看到一個(gè)類型對(duì)象的基本數(shù)據(jù)結(jié)構(gòu)(當(dāng)然,這還不是它的全貌)。詳細(xì)的類型對(duì)象定義:https://github.com/deepkit/deepkit-framework/blob/feature/autotype/packages/type/src/reflection/type.ts#L21-L452
- kind:ReflectionKind,表示傳入的類型。例子中對(duì)應(yīng) Title 的類型
- typeName:string,如果用到了類型別名,會(huì)返回這個(gè)字段,標(biāo)識(shí)該類型
- typeArguments:當(dāng)我們用了泛型時(shí),傳遞進(jìn)去的類型信息也會(huì)保留到類型對(duì)象中,會(huì)返回typeArguments 字段中記錄的就是對(duì)應(yīng)的類型信息。
反射類
反射類多用于 類/接口/對(duì)象類型等等比較復(fù)雜的場(chǎng)景
對(duì)于復(fù)雜場(chǎng)景,我們可以通過(guò) ReflectionClass.from 方法得到類型對(duì)應(yīng)的放射類實(shí)例 ReflectionClass ,通過(guò)調(diào)用ReflectionClass中的方法可以獲取更深層次的類型信息,也可以對(duì)類型信息做一些操作。
驗(yàn)證
需要數(shù)據(jù)驗(yàn)證的場(chǎng)景數(shù)不勝數(shù),接口參數(shù)校驗(yàn),數(shù)據(jù)庫(kù)實(shí)現(xiàn)等都高度依賴數(shù)據(jù)校驗(yàn),以此保證數(shù)據(jù)的安全性。
DeepKit 提供了is和validate兩個(gè)函數(shù),用于校驗(yàn)一個(gè)值是否符合類型定義。
is 函數(shù)接收類型信息,并對(duì)參數(shù)中的數(shù)據(jù)進(jìn)行校驗(yàn),返回一個(gè)布爾值。如上面的例子,定義了一個(gè) People 的 interface,并對(duì) peopleA 和 peopleB 兩個(gè)數(shù)據(jù)進(jìn)行校驗(yàn),可以看出 peopleA 是符合 People 的 定義的,所以返回is<People>(peopleA)?會(huì)返回 true 。peopleB 中的 info 屬性缺少了必填的 phone 字段,因此is<People>(peopleB) 會(huì)返回 false 。
validate 函數(shù)和 is 函數(shù)的用法類似,區(qū)別是 validate 函數(shù)并不是返回一個(gè)布爾值 ,而是一個(gè)包含錯(cuò)誤信息的數(shù)組。
path:錯(cuò)誤路徑,指向出錯(cuò)的具體屬性
code:錯(cuò)誤類型,目前好像只有type 一種。
message:具體的錯(cuò)誤信息。
序列化
DeepKit 中 serialize/deserialize 兩個(gè)方法,為用戶提供了序列化/反序列化的能力
serialize 方法接收類型信息和需要序列化的數(shù)據(jù),將數(shù)據(jù)序列化為符合類型定義的JSON對(duì)象。
deserialize 方法接收類型信息和需要反序列化的數(shù)據(jù),將數(shù)據(jù)反序列化為符合類型信息定義的數(shù)據(jù)。代碼中的 created 字段會(huì)被反序列化為 Date 字段。
類型裝飾器
一句話概括裝飾器:裝飾器本質(zhì)上就是一個(gè)函數(shù),可以在運(yùn)行時(shí)對(duì)被裝飾對(duì)象進(jìn)行自定義的加工處理。
DeepKit 中提供了一套類型裝飾器,這里的類型裝飾器和 TypeScript 的裝飾器并不相同,TypeScript 多用于對(duì)類的裝飾,類型裝飾器顧名思義是對(duì)類型的裝飾。這些類型裝飾器可以被當(dāng)作一個(gè)正常的 TypeScript 類型使用。
舉一個(gè)簡(jiǎn)單的例子
我們對(duì)定義 count 類型為 integer(整型),可以看到,1.1這個(gè)浮點(diǎn)數(shù)類型并沒(méi)有通過(guò)校驗(yàn)。
除此之外,DeepKit 還實(shí)現(xiàn)了如 PrimaryKey(主鍵),maxLength/minLength(最小/最大長(zhǎng)度)等功能的類型裝飾器。我們可以把這些類型裝飾器看作對(duì)于 TypeScript 類型的拓展,這些類型裝飾器使 TypeScript 能夠?qū)崿F(xiàn)數(shù)據(jù)庫(kù)級(jí)別的類型定義。也正是基于這套拓展后的運(yùn)行時(shí)類型,驗(yàn)證和序列化可以有更多的約束,DeepKit 也實(shí)現(xiàn)了一套高性能的 ORM 。
?More?
@deepKit/type 給我們提供了一套運(yùn)行時(shí)調(diào)用類型信息的方案。除此之外,DeepKit 的作者還基于類型信息和反射機(jī)制實(shí)現(xiàn)了更多的能力。
- 事件系統(tǒng):@deepkit/events
- HTTP 庫(kù):@deepkit/http
- RPC服務(wù):@deepkit/rpc
- 數(shù)據(jù)庫(kù)ORM:@deepkit/orm
- 模版引擎:@deepkit/templat ,但與react不兼容
- 大一統(tǒng)框架:@deepkit/framework ,集成了上述能力的 node 框架
?如何保證性能?
為了盡量壓縮運(yùn)行時(shí)的額外開(kāi)銷,DeepKit 的作者做出了不少優(yōu)化。
類型緩存
在未使用泛型的情況下,DeepKit 會(huì)對(duì)使用到的類型對(duì)象進(jìn)行緩存
可以看到,對(duì)于 case1 ,Mytype 對(duì)應(yīng)的類型對(duì)象會(huì)被緩存,因此兩次typeOf<MyType>()? 的結(jié)果相等;但是對(duì)于泛型來(lái)說(shuō),我們無(wú)法確定傳入的 T 具體是什么類型(理論上會(huì)有無(wú)限種),因此不會(huì)結(jié)果進(jìn)行緩存,每次都會(huì)創(chuàng)建一個(gè)新的類型對(duì)象。
類型編譯器
DeepKit 的核心原理是一個(gè)類型編譯器,它會(huì)介入TypeScript 的編譯流程,保留類型信息, 在這個(gè)過(guò)程中,Deepkit 的類型編譯器會(huì)讀取源碼中的類型信息,產(chǎn)生相關(guān)的字節(jié)碼(為了使它盡可能?。⑵洳迦?AST 中,將其轉(zhuǎn)化為另一個(gè)包含這些字節(jié)碼信息的 TypeScript AST。
在運(yùn)行時(shí),DeepKit 會(huì)有一個(gè)迷你虛擬機(jī),負(fù)責(zé)解析和執(zhí)行這些字節(jié)碼,最后會(huì)返回一個(gè)類型對(duì)象。
更詳細(xì)的原理可以參考:https://github.com/microsoft/TypeScript/issues/47658
在 DeepKit 官方提供的性能圖中,可以看到 DeepKit 在數(shù)據(jù)讀寫(xiě)上的表現(xiàn)是比較優(yōu)秀的,這也歸功于 DeepKit 提供的 運(yùn)行時(shí)類型信息,這種預(yù)先知曉類型信息的機(jī)制可以使 序列化/驗(yàn)證等更加快速高效。
?總結(jié)?
DeepKit 是市場(chǎng)上第一個(gè)在 JavaScript 運(yùn)行時(shí)提供全套 TypeScript 類型的解決方案。它使前端/服務(wù)端可以共用一套TypeScript定義的數(shù)據(jù)模型,并且使用基于 TypeScript 實(shí)現(xiàn)的一套反射機(jī)制。
但它依舊存在一些不足,比如 不支持外部類型,若代碼中使用的類型信息來(lái)自第三方,且第三方庫(kù)也沒(méi)有經(jīng)過(guò) deepkit 的類型編譯器的話,外部類型的類型信息在運(yùn)行時(shí)也會(huì)全部丟失。
官方文檔站:https://deepkit.io/
?一些討論?
在TypeScript的倉(cāng)庫(kù)中,其實(shí)已經(jīng)有許多人提出了issue,對(duì)在運(yùn)行時(shí)保留Typescript的類型信息提出了自己的設(shè)想。可以看出,在基于 TypeScript支持動(dòng)態(tài)類型這件事情上,是有需求的,但是 TypeScript 始終是保持保留意見(jiàn),并沒(méi)有實(shí)質(zhì)去支持相關(guān)能力。
個(gè)人的看法,根本上是和 TypeScript 的設(shè)計(jì)目標(biāo)[1] 掛鉤, TypeScript 官方團(tuán)隊(duì)并不希望 TypeScript 會(huì)對(duì)運(yùn)行時(shí)造成額外的開(kāi)銷,并且希望生成的 JavaScript 盡量純凈。TypeScript 官方團(tuán)隊(duì) 的保守嚴(yán)謹(jǐn)造就了 TypeScript 的成功??赡苷蛉绱?,TypeScript 官方團(tuán)隊(duì)才一直對(duì)支持運(yùn)行時(shí)類型持保守態(tài)度。
?參考文獻(xiàn)?
https://deepkit.io/ https://github.com/microsoft/TypeScript/issues/47658