TypeScript 深水區(qū):三種類型來源和三種模塊語法
TypeScript 給 JavaScript 添加了一套類型語法,我們聲明變量的時候可以給變量加上類型信息,這樣編譯階段就可以檢查出變量使用的對不對,也就是類型檢查。
給變量添加類型,很自然可以想到時在聲明的時候指定:
比如對象:
interface Person {
name: string;
age?: number;
}
const guang: Person = {
name: 'guang'
}
比如函數(shù):
function add(num1: number, num2: number): number {
return num1 + num2;
}
這樣當使用它們的時候,比如變量賦值、函數(shù)調用,就可以通過類型信息檢查出使用的對不對:
TypeScript 這樣設計類型語法沒啥問題,但是只是這樣還不夠。
我們自己寫的代碼可以這樣聲明類型,但不是我們寫的呢?
比如 JS 引擎提供的 Number、String、Date、RegExp,瀏覽器環(huán)境的 HTMLElement、Event 等 api。
這些 api 是執(zhí)行引擎內置的實現(xiàn),但我們代碼里會用到它們,也同樣需要檢查使用的對不對,也就是類型檢查。怎么給這些 api 加上類型呢?
TypeScript 類型聲明的三種來源
TypeScript 設計了 declare 的語法,可以單獨聲明變量的類型:
比如對象:
interface Person {
name: string;
age?: number;
}
declare const guang: Person;
比如函數(shù):
declare function add(num1: number, num2: number): number;
這樣單獨聲明了類型,使用這些 api 的時候也就能做類型檢查。
像 JS 引擎那些 api,還有瀏覽器提供的 api,這些基本是必用的,而且都有標準的。所以 TypeScript 給內置了它們的類型聲明。
TypeScript 包下有個 lib 目錄,里面有一堆 lib.xx.d.ts 的類型聲明文件,這就是 TS 內置的一些類型聲明。
因為這些只是聲明類型,而沒有具體的 JS 實現(xiàn),TS 就給單獨設計了一種文件類型,也就是 d.ts, d 是 declare 的意思。
比如 lib.dom.d.ts 里的類型聲明:
因為是 ts 內置的,所以配置一下就可以用了:
tsconfig.json 里配置下 compilerOptions.lib,就可以引入對應的 d.ts 的類型聲明文件。
有的同學可能會說,可是內置的類型聲明也不多呀,只有 dom 和 es 的。
確實,因為 JS 的 api 還有瀏覽器的 api 都是有標準的,那自然可以按照標準來定義類型。其余的環(huán)境的 api 可能沒有標準,經常變,那自然就沒法內置了,比如 node。所以 lib 里只有 dom 和 es 的類型聲明。
那 node 環(huán)境,還有其他環(huán)境里的內置 api 怎么配置類型聲明呢?
node 等環(huán)境的 api 因為沒有標準而沒有被 TS 內置,但 TS 同樣也支持了這些環(huán)境的類型聲明的配置。
方式是通過 @types/xxx 的包:
TS 會先加載內置的 lib 的類型聲明,然后再去查找 @types 包下的類型聲明。
這樣,其他環(huán)境的類型聲明就可以通過這種方式來擴展。
@types 包是在 DefinitelyTyped 這個項目下統(tǒng)一管理的,想創(chuàng)建一個 @types 包的話要去看一下他們的文檔。
一般來說,很快就可以發(fā)到 npm 的:
我們知道,TS 內置的那些 lib 是可以配置的,擴展的這些 @types/xx 的包自然也可以配置:
可以指定加載 @types 目錄下的哪些包,還可以修改查找 @types 包的目錄(默認是 node_modules/@types):
除了給 node 等環(huán)境的 api 加上類型聲明外,@types 包還有一種用途,就是給一些 JS 的包加上類型聲明:
如果代碼本身是用 ts 寫的,那編譯的時候就可以開啟 compilerOptions.declaration,來生成 d.ts 文件:
然后在 package.json 里配置 types 來指定 dts 的位置:
這樣就不需要單獨的 @types 包了。
但如果代碼不是用 ts 寫的,那可能既需要單獨寫一個 @types/xxx 的包來聲明 ts 類型,然后在 tsconfig.json 里配置下,加載進來。
比如常用的 vue3 就不需要 @types/vue 包,因為本身是用 ts 寫的,npm 包里也包含了 dts 文件。
但是 react 不是 ts 寫的,是用的 facebook 自己的 flow,自然就需要 @types/react 的包來加上 ts 類型聲明。
至此,ts 內置的 dom 和 es 的類型聲明,其他環(huán)境還有一些包的類型聲明我們都知道怎么加載了。
那自己寫的 ts 代碼呢?
這些其實我們經常配置,就是配置下編譯的入口文件,通過 includes 指定一堆,然后通過 excludes 去掉一部分。還可以通過 files 再單獨包含一些:
tsc 在編譯的時候,會分別加載 lib 的,@types 下的,還有 include 和 files 的文件,進行類型檢查。
這就是 ts 類型聲明的三種來源。
現(xiàn)在還有一個問題,有的 api 是全局的,有的 api 是某個模塊的,ts 是怎么聲明全局 api 的類型,怎么聲明模塊內的 api 的類型呢?
全局類型聲明 vs 模塊類型聲明
我們寫的 JS 代碼就是有的 api 是全局的,有的 api 是模塊內的,所以 TS 需要支持這個也很正常。
但 JS 的模塊規(guī)范不是一開始就有的,最開始是通過在全局掛一個對象,然后這個對象上再掛一些 api 的方式,也就是命名空間 namespace。
所以 TS 最早支持的模塊化方案自然也就是 namespace:
namespace Guang {
export interface Person {
name: string;
age?: number;
}
const name = 'guang';
const age = 20;
export const guang: Person = {
name,
age
}
export function add(a: number, b: number):number {
return a + b;
}
}
理解 namespace 的話可以看一下編譯后的代碼:
就是全局上放一個對象,然后對象上再掛幾個暴露出去的屬性。
看了編譯后的代碼,是不是 namespace 瞬間就學會了~
后來,出現(xiàn)了 CommonJS 的規(guī)范,那種不能叫 namespace 了,所以 TS 支持了 module,
很容易想到,@types/node 的 api 定義就是一堆的 module:
這個 module 和 namespace 有什么區(qū)別呢?
其實真沒什么區(qū)別,只不過 module 后一般接一個路徑,而 namespace 后一半是一個命名空間名字。其他的語法都一樣的。
而且這個結論是有依據(jù)的:
用 astexplorer.net 看一下 parse 后的 AST,兩者的 AST類型都是一樣的。也就是說編譯器后續(xù)的處理都一樣,那不是一種東西是什么。
再后來的故事大家都知道了,JS 有了 es module 規(guī)范,所以現(xiàn)在推薦直接用 import export 的方式來聲明模塊和導入導出了。
額外多了的,只不過有一個 import type 的語法,可以單獨引入類型:
import type {xxx} from 'yyy';
所以現(xiàn)在聲明模塊不咋推薦用 namespace 和 module,還是盡量用 es module 吧。
那全局的類型聲明呢?
有了 es module 之后,TS 有了一個單獨的設計:
dts 中,如果沒有 import、export 語法,那所有的類型聲明都是全局的,否則是模塊內的。
我們試驗一下:
include 配置 src 下的 ts 文件,然后再用 files 引入 global.d.ts 文件:
在 global.d.ts 里聲明一個 func 函數(shù):
在 src/index.ts 里是有提示的:
編譯也不報錯:
加上一個 import 語句:
編譯就報錯了,說是找不到 func:
這說明 func 就不再是全局的類型了。
這時候可以手動 declare global:
再試一下,編譯就通過了:
而且不止是 es module 的模塊里可以用 global 聲明全局類型,module 的方式聲明的 CommonJS 模塊也是可以的:
比如 @types/node 里就有不少這種全局類型聲明:
這就是 3 種 typescript 聲明模塊的語法,以及聲明全局類型的方式。
那么如果就是需要引入模塊,但是也需要全局聲明類型,有什么更好的方式呢?
有,通過編譯器指令 reference。這樣既可以引入類型聲明,又不會導致所有類型聲明都變?yōu)槟K內的:
可以看到很多 dts 都這樣引入別的 dts 的,就是為了保證引入的類型聲明依然是全局的:
總結
TypeScript 給 JavaScript 添加了類型信息,在編譯時做類型檢查。
除了在變量聲明時定義類型外,TS 也支持通過 declare 單獨聲明類型。只存放類型聲明的文件后綴是 d.ts。
TypeScript 有三種存放類型聲明的地方:
- lib:內置的類型聲明,包含 dom 和 es 的,因為這倆都是有標準的。
- @types/xx:其他環(huán)境的 api 類型聲明,比如 node,還有 npm 包的類型聲明。
- 開發(fā)者寫的代碼:通過 include + exclude 還有 files 指定。
其中,npm 包也可以同時存放 ts 類型,通過 packages.json 的 types 字段指定路徑即可。
常見的是 vue 的類型是存放在 npm 包下的,而 react 的類型是在 @types/react 里的。因為源碼一個是 ts 寫的,一個不是。
巧合的是,TS 聲明模塊的方式也是三種:
- namespace:最早的實現(xiàn)模塊的方式,編譯為聲明對象和設置對象的屬性的 JS 代碼,很容易理解。
- module:和 namespace 的 AST 沒有任何區(qū)別,只不過一般用來聲明 CommonJS 的模塊,在 @types/node 下有很多。
- es module:es 標準的模塊語法,ts 額外擴展了 import type。
dts 的類型聲明默認是全局的,除非有 es module 的 import、export 的聲明,這時候就要手動 declare global 了。為了避免這種情況,可以用 reference 的編譯器指令。
深入掌握 TypeScript 的話,除了學習類型定義以及類型編程,這三種類型聲明的來源(lib、@types、用戶目錄),以及三種模塊聲明的方式(namespace、module、es module),還有全局類型的聲明(global、reference),也都是要掌握的。