TypeScript 的聲明文件的使用與編寫

做者: Angus.Fenying <i.am.x.fenying@gmail.com>node

日期: 2016-09-19 09:53 PMjquery

1. 什麼是聲明文件?

TypeScript 是 JavaScript 的超集,相比 JavaScript,其最關鍵的功能是靜態類型 檢查 (Type Guard)。然而 JavaScript 自己是沒有靜態類型檢查功能的,TypeScript 編譯器也僅提供了 ECMAScript 標準裏的標準庫類型聲明,只能識別 TypeScript 代碼 裏的類型。git

那麼 TypeScript 中如何引用一個 JavaScript 文件呢?例如使用 lodash,async 等 著名的 JavaScript 第三方庫。答案是經過聲明文件(Declaration Files)github

這和 C/C++ 的 *.h 頭文件(Header files)很是類似:當你在 C/C++ 程序中引 用了一個第三方庫(.lib/.dll/.so/.a/.la)時,C/C++ 編譯器沒法自動地識別庫內 導出名稱和函數類型簽名等,這就須要使用頭文件進行接口聲明瞭。shell

同理地,TypeScript 的聲明文件是一個以 .d.ts 爲後綴的 TypeScript 代碼文件, 但它的做用是描述一個 JavaScript 模塊(廣義上的)內全部導出接口的類型信息。express

爲了簡潔,下面把 聲明文件 簡稱爲 Definitionnpm

1.1. 網頁上引用非模塊化的 JavaScript 文件裏的名稱

// <script src="sample-00.js"></script>
// 經過 script 標籤引入名稱到 JS 的全局命名空間中。
var name = "Mick";

function test(inStr) {

    return inStr.substr(0, 4);
}

在另外一個 TypeScript 文件裏引用裏面的名稱,不可用json

// File: test-01.ts
console.log(name); // 編譯報錯,name 不存在。
console.log(test("hello")); // 編譯報錯,test 不存在。

由於 TypeScript 不能從純 JavaScript 文件裏摘取類型信息,因此 TypeScript 的 編譯器根本不知道變量 name 的存在。這一點和 C/C++ 很是類似,而解決方法也幾乎 一致:使用一個 Definition,把這個變量聲明寫進去,讓其它須要使用這個變量的文件引用。c#

// File sample-00.d.ts
declare let name: string;
declare let test: (inStr: string) => string;

在 TypeScript 文件裏使用 三斜線指令 引用這個文件:bash

// File: test-01.ts
/// <reference path="./sample-00.d.ts">
console.log(name); // 編譯經過。
console.log(test("hello")); // 編譯經過。

1.2. 使用第三方庫

第三方庫 async 也是純 JavaScript 庫,沒有類型信息。要在 TypeScript 中使用, 能夠到 DefinitelyTyped 組織的 GitHub 倉庫裏面下載一份 async.d.ts 文件,將之引用進來。

// File: test-02.ts
/// <reference path="./async.d.ts">
import async = require("async");

async.series([

    function(next: ErrorCallback): void {

        console.log(1);
        next();
    },

    function(next: ErrorCallback): void {

        console.log(2);
        next();
    },

    function(next: ErrorCallback): void {

        console.log(3);
        next();
    }

], function(err?: Error): void {

    if (err) {

        console.log(err);

        return;
    }

    console.log("Done");

});

可是一個個庫都去下載對應的 Definition ,實在太麻煩了,也不方便管理,因此咱們能夠 使用 DefinitelyTyped 組織提供的聲明管理器——typings。

2. 使用 typings 聲明管理器

2.1. 安裝與基本使用

typings 是一個用 Node.js 編寫的工具,託管在 NPM 倉庫裏,經過下面的命令能夠安裝

npm install typings -g

如今咱們要安裝 async 庫的 Definition 就簡單了,直接一句命令行

typings install dt~async --global

提示:install 命令能夠縮寫爲 i,且能夠一次安裝多個 Definition 。

參數 --global 意義請參考我另外一篇文章《TypeScript 的兩種聲明文件寫法的 區別和根本意義》。 --global 可簡寫爲 -G

這樣, async 庫的 Definition 就會被安裝到 ./typings/globals/async/index.d.ts

能夠自由地使用 async 庫的 Definition 了。

若是你以爲這個路徑太長了,可使用 ./typings/index.d.ts 這個文件。這是一個 統一索引文件,使用 typings 工具安裝的全部 Definition 都會被引用添加到這個 文件裏,因此經過引用這個文件,就能夠輕鬆引用全部安裝過的 Definition !

2.2. Definition 的源

還有,安裝 Definition 命令裏的 dt~async 是什麼東西?async 固然是一個庫的 名稱。那 dt 呢?其實 dt 是指,表示這個 Definition 的來源。目前絕大 多數的庫 Definition 都是託管在 DefinitelyTyped 項目的 GitHub 倉庫裏面的,因此 使用 dt~庫名稱 能夠找到絕大部分庫的 Definition 。

若是你不肯定某個庫 Definition 的源,可使用下面的命令查找

typings search --name 庫準確名稱

一個輸出例子是:

$ typings search --name jquery
Viewing 1 of 1

NAME   SOURCE HOMEPAGE           DESCRIPTION VERSIONS UPDATED
jquery dt     http://jquery.com/             1        2016-09-08T20:32:39.000Z

能夠看出,jquery 庫 Definition 信息是存在的,對應的 源(SOURCE) 是 dt

2.3. 安裝某個庫特定版本的 Definition

2016 年 9 月初,不少人發現經過 typings 安裝的 env~node 在 TS 編譯輸出爲 ES5 標準的狀況下不可用了,編譯報錯。緣由是 DefinitelyTyped 庫將 env~node 的最新 版本更新爲 6.0 版本,只支持 ES6 標準了。這致使不少編譯目標爲 ES5 甚至 ES3 的項目 都由於沒法識別裏面的 ES6 標準元素而出錯。

解決方案是安裝特定的兼容分支便可,如何安裝特定版本的 Definition 呢?首先,經過 typings 工具的 info 命令查看某個庫聲明的分支信息。例如:

$ typings info env~node --versions
TAG                   VERSION LOCATION                                                            UPDATED
6.0.0+20160902022231  6.0.0   github:types/env-node/6#30804787ed04e4d475046ef0335bef502f492da0    2016-09-02T02:22:31.000Z
4.0.0+20160902022231  4.0.0   github:types/env-node/4#30804787ed04e4d475046ef0335bef502f492da0    2016-09-02T02:22:31.000Z
0.12.0+20160902022231 0.12.0  github:types/env-node/0.12#30804787ed04e4d475046ef0335bef502f492da0 2016-09-02T02:22:31.000Z
0.11.0+20160902022231 0.11.0  github:types/env-node/0.11#30804787ed04e4d475046ef0335bef502f492da0 2016-09-02T02:22:31.000Z
0.10.0+20160902022231 0.10.0  github:types/env-node/0.10#30804787ed04e4d475046ef0335bef502f492da0 2016-09-02T02:22:31.000Z
0.8.0+20160902022231  0.8.0   github:types/env-node/0.8#30804787ed04e4d475046ef0335bef502f492da0  2016-09-02T02:22:31.000Z

能夠看到 env~node 有 6 個分支(Tag),對應 Node.js 的不一樣版本。

這些分支對 Node.js 是版本號,但對於 typings 它們都是分支,而不是版本!

而後經過

typings i env~node#4.0.0+20160902022231 --global

就安裝好了。

2.4. 從 GitHub 倉庫安裝 Definition

可使用 typings 從指定的 GitHub 倉庫裏下載安裝 Definition

命令格式有兩種:

# 文件式
typings i github:用戶名/項目名稱/文件路徑 --global

# 倉庫式
typings i github:用戶名/項目名稱 --global

2.4.1. 直接安裝倉庫裏的某個文件做爲 Definition

# 文件式
# 安裝這個文件的最新 commit 版本
typings i github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts --global

2.4.2. 使用特定 commit 版本做爲 Definition

# 文件式
# 安裝這個文件的 commit=5fd6d6b4eaabda87d19ad13b7d6709443617ddd8 的版本
typings i github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#5fd6d6b4eaabda87d19ad13b7d6709443617ddd8 --global

2.4.3. 使用專用的 GitHub 倉庫

假設我爲一個叫 ABCDEFG 的庫寫了一個 Definition,如今我要把它發佈到 GitHub 上做 爲 typings 源。那麼先創建一個 GitHub 項目,名字隨意,這裏假設是 https://github.com/sample/abcdefg-typings

把 Definition 取名爲 index.d.ts,再添加一個文件 typings.json,內容以下:

{
  "name": "abcdefg",
  "main": "index.d.ts",
  "version": "0.1.0-demo"
}

index.d.tstypings.json 兩個文件提交到 GitHub 的 sample/abcdefg-typings 倉庫。如今,咱們能夠經過下面的命令安裝了。

# 倉庫式
# 安裝這個倉庫的最新 commit 版本
typings i github:sample/abcdefg-typings --global

安裝成功後能夠看到控制檯提示

typings WARN badlocation "github:sample/abcdefg-typings" is mutable and may change, consider specifying a commit hash
abcdefg-typings@0.1.0-demo
`-- (No dependencies)

那句警告的意思是建議使用一個 commit ID,這個就隨意了。

2.5. 使用 typings.json 管理 Definition

看了上面的用法,爲了更方便的管理一個項目依賴的 Definition (好比更新版本), typings 須要使用一個名爲 typings.json 文件來記錄咱們安裝過的 Definition 。

先初始化它,

typings init

這個命令初始化了 typings.json 文件,內容是一個空的 Definition 依賴記錄表:

{
  "dependencies": {}
}

如今咱們來安裝 Definition ,並記錄到表中:

typings i env~node dt~async --global --save

後面的 --save(可簡寫爲 -S) 會將 Definition 信息添加到 Definition 依賴記錄表, 好比如今的 typings.json 文件內容以下:

{
  "dependencies": {},
  "globalDependencies": {
    "async": "registry:dt/async#2.0.1+20160804113311",
    "node": "registry:dt/node#6.0.0+20160915134512"
  }
}

這樣,發佈項目時或者上傳代碼到 GitHub 的時候,typings 目錄就無關緊要了,須要的 時候直接一句 typings i 就完成了 Definition 的安裝。須要注意的是,typings 默認安裝最新版本的 Definition,若是你不想每次都安裝最新的,能夠經過 2.4. 從 GitHub 倉庫安裝 Definition 的方法解決。

3. 編寫 Definition

前面講了不少關於如何使用 Definition 的內容,那都是「用」,下面來說講如何本身寫 一個 Definition。

3.1 Node.js 與 NPM 模塊

NPM 在某個項目內本地安裝的模塊都在項目的 ./node_modules 目錄下,一個模塊一個 目錄,以模塊名稱爲目錄名。

對於一個 NPM 模塊,經過裏面的 package.json 文件的 main 字段能夠指定其默認的 入口文件。在 Node.js 裏經過 require("模塊名稱") 引用的就是這個默認的入口 文件。若是未指定 package.json 文件的 main 字段,可是存在 index.js 文件, 那麼 index.js 也會被當成默認的入口文件。

除此以外,在 Node.js 裏面還能夠單獨引用 NPM 模塊的其中一個文件,而不僅是經過 默認入口文件引用模塊。例如:

var sample = require("sample");
var lib1 = require("sample/lib1");
var lib2 = require("sample/lib2");

如今假設這三個文件的代碼以下,咱們將在後面以這三個文件爲基礎編寫 Definition:

// File: ./node_modules/sample/index.js
var abc = 321;
exports.setABC = function(abcValue) {
    abc = abcValue;
};
exports.getABC = function() {
    return abc;
};

exports.defaultABC = abc;
// File: ./node_modules/sample/lib1.js
var Hello = (function () {
    function Hello(a) {
        this.valueA = a;
    }
    Object.defineProperty(Hello.prototype, "a", {
        get: function () {
            return this.valueA;
        },
        enumerable: true,
        configurable: true
    });
    Hello.initClass = function () {
        Hello.instCount = 0;
    };
    /**
     * 假設這是一個重載函數,支持多種調用方式
     */
    Hello.prototype.setup = function (x, b) {
        if (b === void 0) { b = null; }
        return false;
    };
    return Hello;
}());
exports.Hello = Hello;
// File: ./node_modules/sample/lib2.js

var randStrSeed = "abcdefghijklmnopqrstuvwxyz0123456789";

function randomString(length) {

    var ret = "";

    while (length-- > 0) {

        ret += randStrSeed[Math.floor(Math.random() * randStrSeed.length)];
    }

    return ret;
}

module.exports = randomString;

這是三個典型的模塊類型,第一個導出了變量和函數,第二個導出了一個類,第三個則將 一個函數做爲一個模塊導出。

如今咱們以這三個文件爲例,分別以模塊導出聲明 (External Module Definition)全局類型聲明(Global Type Definition) 兩種寫法編寫 Definition。

3.2. 全局類型聲明寫法

假如上面的3個文件同屬一個模塊 sample,可是它並非咱們本身在 NPM 上發佈的, 便是說咱們無權給它添加內建 Definition。因此咱們用全局類型聲明寫法。

這是一個不是很複雜的模塊,那麼咱們用一個 .d.ts 文件就能夠了。

第一個文件是模塊的入口文件,能夠直接當成模塊 sample。定義以下:

declare module "sample" {

    // 導出函數 setABC
    export function setABC(abcValue: number): void;

    // 導出函數 getABC
    export function getABC(): number;

    // 導出變量 defaultABC
    export let defaultABC: number;
}

第二個文件是導出了兩個類,能夠當成模塊 "sample/lib1"。下面來看看如何導出類。

這個類裏面有構造函數,有靜態方法,有普通方法,有屬性,也有靜態屬性,還有 getter。

類有兩種聲明編寫方式:標準式分離式

標準式很直接,就是像 C/C++ 的頭文件裏聲明類同樣只寫類聲明不寫實現:

declare module "sample/lib1" {

    export class Hello {

        private valueA;

        b: number;

        static instCount: number;

        a: number;

        constructor(a: number);

        static initClass(): void;

        /**
        * 假設這是一個重載函數,支持多種調用方式
        */
        setup(name: string): boolean;

        setup(name: string, age: number): boolean;
    }
}

可是這種寫法也有不便的地方,好比擴展類不方便——JavaScript容許你隨時擴展一個類的原型 對象實現對類的擴展,或者隨時給類添加靜態成員。標準式寫法很難實現擴展,由於你沒法 重複聲明一個類。

因此下面咱們來看看所謂的分離式聲明。在這以前咱們要理解,JS 的類是用函數實現的, 便是說 JS 的類本質上就是一個構造函數 + Prototype。Prototype 的成員就是類的成員; 而類的靜態方法就是這個構造函數對象自己的成員方法。

所以咱們能夠分開寫這二者的聲明:

declare module "sample/lib1" {

    /**
     * 在分離式寫法裏面,一個類的 Prototype 的聲明是一個直接以類名稱爲名的
     * interface。咱們把成員函數和變量/getter/setter 都行寫在 prototype
     * 的接口裏面。
     *
     * 注意:類原型的 interface 取名與類名一致。 
     */
    export interface Hello {

        /**
         * 接口裏面只寫類的 public 屬性
         */
        "b": number;

        /**
         * Getter/Setter 直接成屬性便可。
         */
        "a": number;

        /**
         * 重載函數的聲明寫法
         */ 
        setup(name: string): boolean;
        setup(name: string, age: number): boolean;
    }

    /**
     * 在分離式寫法裏面,一個類的構造函數對象也是一個 interface ,可是對
     * 其命名無具體要求,合理便可。
     * 
     * 把類的靜態方法和屬性都寫在這裏面。
     */
    export interface HelloConstructor {

        /**
         * 靜態屬性
         */
        "instCount": number;

        /**
         * 靜態方法
         */
        initClass(): void;

        /**
         * 構造函數!
         * 使用 new 代替 constructor,並聲明其返回值類型是該類的Prototype。
         */
        new(a: number): Hello;
    }

    /**
     * 將 Hello 覆蓋聲明爲 HelloConstructor。
     * 
     * 這樣,在須要做爲類使用的時候它就是 HelloConstructor,
     * 須要做爲接口使用的時候就是 Hello(原型接口)。
     */
    export let Hello: HelloConstructor;
}

如上,就是導出類的兩種姿式~

接着看第三個文件,直接將一個函數做爲模塊導出,也是很簡單的。

declare module "sample/lib2" {

    let randomString: (length: number) => string;

    export = randomString;
}

最後把 3 個模塊的聲明合併成一個文件 sample.d.ts,在文件裏用三斜線指令引用便可。

3.3. 模塊導出聲明寫法

模塊導出聲明寫法裏面不用註明是哪一個模塊,通常給每一個導出的文件都配備一個以 .d.ts 爲後綴的 Definition。

  • 文件 ./node_modules/sample/index.d.ts

    // File: ./node_modules/sample/index.d.ts
    // 導出函數 setABC
    export declare function setABC(abcValue: number): void;
    
    // 導出函數 getABC
    export declare function getABC(): number;
    
    // 導出變量 defaultABC
    export declare let defaultABC: number;
  • 文件 ./node_modules/sample/lib1.d.ts

    // File: ./node_modules/sample/lib1.d.ts
    
    export class Hello {
    
        private valueA;
    
        b: number;
    
        static instCount: number;
    
        a: number;
    
        constructor(a: number);
    
        static initClass(): void;
    
        /**
        * 假設這是一個重載函數,支持多種調用方式
        */
        setup(name: string): boolean;
    
        setup(name: string, age: number): boolean;
    }
  • 文件 ./node_modules/sample/lib2.d.ts

    // File: ./node_modules/sample/lib2.d.ts
    
    let randomString: (length: number) => string;
    
    export = randomString;

3.4. 如何肯定現有類的聲明接口名稱?

以確認 String 類的聲明接口名稱爲例。

在 TypeScript 源碼的 lib.d.ts 裏面能夠找到:

declare var String: StringConstructor;

這就是 String 類的構造函數了,便是說 StringConstructor 定義了 String 的靜態方法。

使用如 Visual Studio Code 的編輯器,在裏面隨意打開一個 *.ts 文件, 而後輸入好比 String ,鼠標移動上去,能夠看到類型定義。

而後查看 StringConstructor 的定義:

/*
 * 全局類/對象的聲明都是在 lib.d.ts 裏面定義的,便是說 TypeScript 一般會
 * 默認引用一個 lib.d.ts 文件,因此這裏面的內容無需引用聲明便可使用。
 *
 * 也正所以 StringConstructor 不須要 declare 和 export。
 *
 */
interface StringConstructor {
    new (value?: any): String;
    (value?: any): string;
    prototype: String;
    fromCharCode(...codes: number[]): string;
}

這裏能夠看出,String 類的構造函數的聲明是接口 StringConstructor, 而其 String.prototype 的聲明是接口 String,顯然用了分離式寫法。

interface String {

    //...
}

3.5. 擴展 JavaScript 全局類/對象

前面咱們實現了一個模塊的聲明文件。

以 langext 的代碼爲例,試圖爲 JS 原生的 String 類添加一個 random 靜態方法。

若是直接寫:

String.random = function(len: number): string {

    return '...';
};

是沒法經過編譯的,由於 TS 的類型檢查,根據既有的 String 類定義,發現 random 不是 String 類的靜態成員。

解決方法是使用一個聲明文件,在裏面寫:

interface StringConstructor {

    random(length: number): string;
}

而後引用這個定義文件便可。

這是利用了 TS 的 interface 可分離定義特性,同名的 interface,只要字段定義不衝突 就能夠分開定義。【參考 4.2 節】

4. 編寫 Definition 的注意事項

4.1. 不要使用內層 declare

只能在 Definition 的頂層使用 declare,好比下面的寫法都是錯誤的:

declare module "sample" {

    // 此處應當使用 export
    declare let a: string;
}

declare namespace Sample {

    // 此處應當使用 export
    declare let a: string;
}

4.2. 避免全局污染

雖然全局聲明寫法容許你引入名稱到全局命名空間中,但這也意味着,引入的頂層名稱 都是全局的。因此應該將全部的模塊內導出的元素都放進模塊或者命名空間內:

declare module "sample" {

    /**
     * 僅可經過 import { Person } from "sample" 訪問。
     */
    export interface Person {

        name: string;
    }
}

declare namespace Sample {

    export interface Animal {

        type: string;
    }
}

而不是

/**
 * 無需 import 便可使用,即全局的
 */
interface Person {

    name: string;
}

不過如下狀況例外:

  1. 當在擴展全局對象/類的時候,容許這麼寫

    interface StringConstructor {
    
        random(length: number): string;
    }
  2. 當確實引入了新的全局名稱時,好比 script 裏的全局變量

    declare let globalName: string;

4.3. 注意聲明衝突

module 和 namespace 都是能夠重複聲明的——可是裏面的元素不能衝突。具體以下:

declare module "sample" {

    export let name: string;

    export interface ABC {

        value: string;
    }
}

declare module "sample" {

    // 衝突,由於 sample 模塊裏已經有了導出變量 name
    export let name: string;

    // 不衝突,由於兩個內容不重複的重名 interface 能夠合併。
    export interface ABC {

        
        name: string;
    }
}

declare module "sample" {

    // 衝突,由於前面的 sample.ABC 裏面已經定義了 value 字段。
    export interface ABC {

        
        value: string;
    }
}

4.4. 模塊名稱要區分大小寫!

這一點對於 Windows 上的 Node.js 開發人員很致命!由於在 Windows 下文件名不區分 大小寫,因此你不區分大小寫均可以成功引用模塊, 可是,Node.js 並不認爲僅僅名稱大小寫不一致的兩個文件是同一個模塊!

這將致使一個嚴重的後果——同一個模塊被初始化爲不一樣名稱(大小寫不一致)的多個實例, 致使各處引用的不是同一個實例,從而形成數據不一樣步。

相關文章
相關標籤/搜索