JavaScript 和 TypeScript 交叉口 —— 類型定義文件(*.d.ts)


《從 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
}

interfacetype(或者說 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'
  } 
}

基本上頂層的定義都需要使用 declareclass 也是:

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
}

類型遍歷

當你已知某個類型范圍的時候,可以使用 inkeyof 來遍歷類型,例如上面的 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

  1. 與你的 npm 包捆綁在一起(內置類型定義文件)
  2. 發布到 npm 上的 @types organization

前者,安裝完了包之后會自動檢測並識別類型定義文件。
后者,則需要通過 npm i @types/xxxx 安裝,這就是我們前面所說的 DefinitelyTyped ,用於擴展 JS 庫的類型聲明。

內置類型定義文件

內置類型定義就是把你的類型定義文件和 npm 包一起發布,一般來說,類型定義文件都放在包根目錄的 types 目錄里,例如 vue

如果你的包有一個主 .js 文件,需要在 package.json 里指定主類型定義文件。

設置 typestypeings 屬性指向捆綁在一起的類型定義文件。 例如包目錄如下:

├── 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 到 https://github.com/DefinitelyTyped/DefinitelyTyped

在這里查看詳細信息 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

入口模塊主要做這些事情:

  1. 定義命名空間
  2. 導出和聚合子模塊

主出口文件 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
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM