這篇文章有更新!!!!請去 原文 閱讀!!!
在 《從 JavaScript 到 TypeScript 系列》 文章咱們已經學習了 TypeScript 相關的知識。
TypeScript 的核心在於靜態類型,咱們在編寫 TS 的時候會定義不少的類型,可是主流的庫都是 JavaScript 編寫的,並不支持類型系統。那麼如何讓這些第三方庫也能夠進行類型推導呢?javascript
這篇文章咱們來說解 JavaScript 和 TypeScript 的靜態類型交叉口 —— 類型定義文件。css
這篇文章首發於個人我的博客 《據說》。html
前端開發 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 庫也能定義靜態類型。vue
TypeScript 通過了一系列的摸索,前後提出了 tsd(已廢棄)、typings(已廢棄),最終在 TypeScript 2.0 的時候從新整理了類型定義,提出了 DefinitelyTyped。java
DefinitelyTyped 就是讓你把 "類型定義文件(*.d.ts)",發佈到 npm
中,配合編輯器(或插件),就可以檢測到 JS 庫中的靜態類型。jquery
類型定義文件的以 .d.ts
結尾,裏面主要用來定義類型。git
例如這是 jQuery 的類型定義文件 中一段代碼(爲了方便理解作了一些改動)github
// 定義 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 良好的靜態類型系統洗禮事後,語法學習成本很是低。web
咱們可使用 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
能夠建立 *.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
須要 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 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 中。例如 @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。
一般來講,若是這份類型定義文件是 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 }