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

《從 JavaScript 到 TypeScript 系列》 文章咱們已經學習了 TypeScript 相關的知識。
TypeScript 的核心在於靜態類型,咱們在編寫 TS 的時候會定義不少的類型,可是主流的庫都是 JavaScript 編寫的,並不支持類型系統。那麼如何讓這些第三方庫也能夠進行類型推導呢?javascript

這篇文章咱們來說解 JavaScript 和 TypeScript 的靜態類型交叉口 —— 類型定義文件。css

這篇文章首發於個人我的博客 《據說》。html

前端開發 QQ 羣:377786580前端

類型定義文件

在 TypeScript 中,咱們能夠很簡單的,在代碼編寫中定義類型:vue

interface IBaseModel {
  say(keys: string[] | null): object
}

class User implements IBaseModel {
  name: string
  constructor (name: string) {
    this.name = name
  }
}

可是主流的庫都是 JavaScript 編寫的,TypeScript 身爲 JavaScript 的超集,天然須要考慮到如何讓 JS 庫也能定義靜態類型。java

TypeScript 通過了一系列的摸索,前後提出了 tsd(已廢棄)、typings(已廢棄),最終在 TypeScript 2.0 的時候從新整理了類型定義,提出了 DefinitelyTypedjquery

DefinitelyTyped 就是讓你把 "類型定義文件(*.d.ts)",發佈到 npm 中,配合編輯器(或插件),就可以檢測到 JS 庫中的靜態類型。git

類型定義文件的以 .d.ts 結尾,裏面主要用來定義類型。github

例如這是 jQuery 的類型定義文件 中一段代碼(爲了方便理解作了一些改動)web

// 定義 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
}
相關文章
相關標籤/搜索