在 《從 JavaScript 到 TypeScript 系列》 文章我們已經學習了 TypeScript 相關的知識。
TypeScript 的核心在於靜態型別,我們在編寫 TS 的時候會定義很多的型別,但是主流的庫都是 JavaScript 編寫的,並不支援型別系統。那麼如何讓這些第三方庫也可以進行型別推導呢?
這篇文章我們來講解 JavaScript 和 TypeScript 的靜態型別交叉口 —— 型別定義檔案。
這篇文章首發於我的個人部落格 《聽說》。
前端開發 QQ 群:377786580
型別定義檔案
在 TypeScript 中,我們可以很簡單的,在程式碼編寫中定義型別:
interface IBaseModel {
say(keys: string[] | null): object
}
class User implements IBaseModel {
name: string
constructor (name: string) {
this.name = name
}
}複製程式碼
但是主流的庫都是 JavaScript 編寫的,TypeScript 身為 JavaScript 的超集,自然需要考慮到如何讓 JS 庫也能定義靜態型別。
TypeScript 經過了一系列的摸索,先後提出了 tsd(已廢棄)、typings(已廢棄),最終在 TypeScript 2.0 的時候重新整理了型別定義,提出了 DefinitelyTyped。
DefinitelyTyped 就是讓你把 "型別定義檔案(*.d.ts)",釋出到 npm
中,配合編輯器(或外掛),就能夠檢測到 JS 庫中的靜態型別。
型別定義檔案的以 .d.ts
結尾,裡面主要用來定義型別。
例如這是 jQuery 的型別定義檔案 中一段程式碼(為了方便理解做了一些改動)
// 定義 jQuery 需要用到的型別名稱空間
declare namespace JQuery {
// 定義基本使用的型別
type Selector = string;
type TypeOrArray<T> = T | T[];
type htmlString = string;
}
// 定義 jQuery 介面,jquery 是一個 包含 Element 的集合
interface JQuery<TElement extends Node = HTMLElement> extends Iterable<TElement> {
length: number;
eq(index: number): this;
// 過載
add(selector: JQuery.Selector, context: Element): this;
add(selector: JQuery.Selector | JQuery.TypeOrArray<Element> | JQuery.htmlString | JQuery): this;
children(selector?: JQuery.Selector): this;
css(propertyName: string): string;
html(): string;
}
// 對模組 jquery 輸出介面
declare module 'jquery' {
// module 中要使用 export = 而不是 export default
export = jQuery;
}複製程式碼
型別定義
*.d.ts
編寫起來非常簡單,經過 TypeScript 良好的靜態型別系統洗禮過後,語法學習成本非常低。
我們可以使用 type
用來定義型別變數:
// 基本型別
type UserName = string
// 型別賦值
type WebSite = string
type Tsaid = WebSite複製程式碼
可以看到 type
其實可以定義各種格式的型別,也可以和其他型別進行組合。
// 物件
type User = {
name: string;
age: number;
website: WebSite;
}
// 方法
type say = (age: number) => string
// 類
class TaSaid {
website: string;
say: (age: number) => string;
}複製程式碼
當然,我們也可以使用 interface
定義我們的複雜型別,在 TS 中我們也可以直接定義 interface
:
interface Application {
init(): void
get(key: string): object
}複製程式碼
interface
和 type
(或者說 class
) 很像。
但是 type
的含義是定義自定義型別,當 TS 提供給你的基礎型別都不滿足的時候,可以使用 type
自由組合出你的新型別,而 interface
應該是對外輸出的介面。
type
不可以被繼承,但 interface
可以:
interface BaseApplication {
appId: number
}
export interface Application extends BaseApplication {
init(): void
get(key: string): object
}複製程式碼
declare
declare
可以建立 *.d.ts
檔案中的變數,declare
只能作用域最外層:
declare var foo: number;
declare function greet(greeting: string): void;
declare namespace tasaid {
// 這裡不能 declare
interface blog {
website: 'http://tasaid.com'
}
}複製程式碼
基本上頂層的定義都需要使用 declare
, class
也是:
declare class User {
name: string
}複製程式碼
namespace
為防止型別重複,使用 namespace
用於劃分割槽域塊,分離重複的型別,頂層的 namespace
需要 declare
輸出到外部環境,子名稱空間不需要 declare
。
// 名稱空間
declare namespace Models {
type A = number
// 子名稱空間
namespace Config {
type A = object
type B = string
}
}
type C = Models.Config.A複製程式碼
組合定義
上面我們只演示了一些簡單的型別組合,生產環境中會包含許多複雜的型別定義,這時候我們就需要各種組合出強大的型別定義:
動態屬性
有些型別的屬性名是動態而未知的,例如:
{
'10086': {
name: '中國移動',
website: 'http://www.10086.cn',
},
'10010': {
name: '中國聯通',
website: 'http://www.10010.com',
},
'10000': {
name: '中國電信',
website: 'http://www.189.cn'
}
}複製程式碼
我們可以使用動態屬性名來定義型別:
interface ChinaMobile {
name: string;
website: string;
}
interface ChinaMobileList {
// 動態屬性
[phone: string]: ChinaMobile
}複製程式碼
型別遍歷
當你已知某個型別範圍的時候,可以使用 in
和 keyof
來遍歷型別,例如上面的 ChinaMobile 例子,我們可以使用 in
來約束屬性名必須為三家運營商之一:
type ChinaMobilePhones = '10086' | '10010' | '10000'
interface ChinaMobile {
name: string;
website: string;
}
// 只能 type 使用, interface 無法使用
type ChinaMobileList = {
// 遍歷屬性
[phone in ChinaMobilePhones]: ChinaMobile
}複製程式碼
我們也可以用 keyof
來約定方法的引數
export type keys = {
name: string;
appId: number;
config: object;
}
class Application {
// 引數和值約束範圍
set<T extends keyof keys>(key: T, val: keys[T])
get<T extends keyof keys>(key: T): keys[T]
}複製程式碼
整合釋出
有兩種主要方式用來發布型別定義檔案到 npm
:
- 與你的 npm 包捆綁在一起(內建型別定義檔案)
- 釋出到 npm 上的 @types organization
前者,安裝完了包之後會自動檢測並識別型別定義檔案。
後者,則需要通過 npm i @types/xxxx
安裝,這就是我們前面所說的 DefinitelyTyped ,用於擴充套件 JS 庫的型別宣告。
內建型別定義檔案
內建型別定義就是把你的型別定義檔案和 npm 包一起釋出,一般來說,型別定義檔案都放在包根目錄的 types
目錄裡,例如 vue:
如果你的包有一個主 .js
檔案,需要在 package.json
裡指定主型別定義檔案。
設定 types
或 typeings
屬性指向捆綁在一起的型別定義檔案。 例如包目錄如下:
├── lib
│ ├── main.js
│ └── main.d.ts # 型別定義檔案
└── package.json複製程式碼
// pageage.json
{
"name": "demo",
"author": "demo project",
"version": "1.0.0",
"main": "./lib/main.js",
// 定義主型別定義檔案
"types": "./lib/main.d.ts"
}複製程式碼
如果主型別定義檔名是 index.d.ts
並且位置在包的根目錄裡,就不需要使用 types
屬性指定了。
├── lib
│ └── main.js
├── index.d.ts # 型別定義檔案
└── package.json複製程式碼
如果你發的包中,package.json
中使用了 files
欄位的話(npm
會根據 files
配置的規則決定釋出哪些檔案),則需要手動把型別定義檔案加入:
// pageage.json
{
"files": [
"index.js",
"*.d.ts"
]
}複製程式碼
如果只發二級目錄的話,把型別定義檔案放到對應的二級目錄下即可:
import { default as App } from 'demo/app'複製程式碼
釋出到 @types organizatio
釋出到 @types organizatio
的包表示源包沒有包含型別定義檔案,第三方/或原作者定義好型別定義檔案之後,釋出到 @types 中。例如 @types/express。
根據 DefinitelyTyped
的規則,和編輯器(和外掛) 自動檢測靜態型別。
@types 下面的包是從 DefinitelyTyped 裡自動釋出的,通過 types-publisher 工具。
如果想讓你的包釋出為 @types 包,需要提交一個 pull request 到 github.com/DefinitelyT…。
在這裡檢視詳細資訊 contribution guidelines page。
如果你正在使用 TypeScript,而使用了一些 JS 包並沒有對應的型別定義檔案,可以編寫一份然後提交到 @types
。
贈人玫瑰,手留餘香。
釋出到 @types organizatio
的包可以通過 TypeSearch 搜尋檢索,使用 npm install --save-dev @types/xxxx
安裝:
更多細節請參閱 DefinitelyTyped。
其他
module
通常來說,如果這份型別定義檔案是 JS 庫自帶的,那麼我們可以直接匯出模組:
interface User {}
export = User複製程式碼
而如果這份型別定義檔案不是 JS 庫自帶的,而是第三方的,則需要使用 module
進行關聯。
例如 jquery
釋出的 npm 包中不包含 *.d.ts
型別定義檔案,jquery
的型別定義檔案釋出在了 @types/jquery
,所以型別定義檔案中匯出型別的時候,需要關聯模組 jquery
,意思就是我專門針對這個包做的型別定義:
interface jQuery {}
declare module 'jquery' {
// module 中要使用 export = 而不是 export default
export = jQuery;
}複製程式碼
從而解決了一些主流的 JS 庫釋出的 npm
包中沒有型別定義檔案,但是我們可以用第三方型別定義檔案為這些庫補充型別。
風格
經過一系列探索,個人比較推薦下面的編寫風格,先看目錄:
types
├── application.d.ts
├── config.d.ts
├── index.d.ts # 入口模組
└── user.d.ts複製程式碼
入口模組主要做這些事情:
- 定義名稱空間
- 匯出和聚合子模組
主出口檔案 index.d.ts
:
import * as UserModel from './user'
import * as AppModel from './application'
import * as ConfigModel from './config'
declare namespace Models {
export type User = UserModel.User;
export type Application = AppModel.Application;
// 利用 as 抹平爭議性變數名
export type Config = ConfigModel.Config;
}複製程式碼
子模組無需定義名稱空間,這樣外部環境 (types
資料夾之外) 則無法獲取子模組型別,達到了型別封閉的效果:
export interface User {
name: string;
age: number
}複製程式碼