TypeScript聲明文件的語法與場景詳解

簡介

聲明文件是以.d.ts為後綴的文件,開發者在聲明文件中編寫類型聲明,TypeScript根據聲明文件的內容進行類型檢查。(註意同目錄下最好不要有同名的.ts文件和.d.ts,例如lib.ts和lib.d.ts,否則模塊系統無法隻根據文件名加載模塊)

為什麼需要聲明文件呢?我們知道TypeScript根據類型聲明進行類型檢查,但有些情況可能沒有類型聲明:

  • 第三方包,因為第三方包打包後都是JavaScript語法,而非TypeScript,沒有類型。
  • 宿主環境擴展,如一些hybrid環境,在window變量下有一些bridge接口,這些接口沒有類型聲明。

如果沒有類型聲明,在使用變量、調用函數、實例化類的時候就沒法通過TypeScript的類型檢查。

聲明文件就是針對這些情況,開發者在聲明文件中編寫第三方模塊的類型聲明/宿主環境的類型聲明。讓TypeScript可以正常地進行類型檢查。

除此之外,聲明文件也可以被導入,使用其中暴露的類型定義。

總之,聲明文件有兩種用法:

  • 被通過import導入,使用其中暴露的類型定義和變量聲明。
  • 和相關模塊關聯,為模塊進行類型聲明。

對於第二種用法,聲明文件如何同相關模塊關聯呢?

比如有個第三方包名字叫"foo",那麼TypeScript會在node_modules/foo中根據其package.json的types和typing字段查找聲明文件查找到的聲明文件被作為該模塊的聲明文件;TypeScript也會在node_modules/@types/foo/目錄中查找聲明文件,如果能找到就被作為foo模塊的聲明文件;TypeScript還會在我們的項目中查找.d.ts文件,如果遇到declare module 'foo'語句,則該聲明被用作foo模塊的聲明。

總結一下,TypeScript會在特定的目錄讀取指定的聲明文件。

  • 在內部項目中,TypeScript會讀取tsconfig.json中的文件集合,在其中的聲明文件才會被處理。
  • 讀取node_modules中各第三方包的package.json的types或者typing指定的文件。
  • 讀取@types目錄下同名包的聲明文件。

聲明文件中的代碼不會出現在最終的編譯結果中,編譯後會把轉換後的JavaScript代碼輸出到"outDir"選項指定的目錄中,並且把 .ts模塊中使用到的值的聲明都輸出到"declarationDir"指定的目錄中。

而在.ts文件中的聲明語句,編譯後會被去掉,如

declare let a: number;

export default a;

會被編譯為

"use strict";
exports.__esModule = true;
exports["default"] = a;

TypeScript編譯過程不僅將TypeScript語法轉譯為ES6/ES5,還會將代碼中.ts文件中用到的值的類型輸出到指定的聲明文件中。如果你需要實現一個庫項目,這個功能很有用,因為用到你的庫的項目可以直接使用這些聲明文件,而不需要你再為你的庫寫聲明文件。

語法

內容

TypeScript中的聲明會創建以下三種實體之一:命名空間,類型或值。

命名空間最終被編譯為全局變量,因此我們也可以認為聲明文件中其實創建瞭類型和值兩種實體。即定義類型或者聲明值。

// 類型 接口
interface Person {name: string;}

// 類型 類型別名
type Fruit = {size: number};

// 值 變量
declare let a: number;

// 值 函數
declare function log(message: string): void;

// 值 類
declare class Person {name: string;}

// 值 枚舉
declare enum Color {Red, Green}

// 值 命名空間
declare namespace person {let name: string;}

我們註意到類型可以直接定義,但是值的聲明需要借助declare關鍵字,這是因為如果不用declare關鍵字,值的聲明和初始化是一起的,如

let a: number;

// 編譯為
var a;

但是編譯結果是會去掉所有的聲明語句,保留初始化的部分,而聲明文件中的內容隻是起聲明作用,因此需要通過declare來標識,這隻是聲明語句,編譯時候直接去掉即可。

TypeScript也約束聲明文件中聲明一個值必須要用declare,否則會被認為存在初始化的內容,從而報錯。

// foo.d.ts
let a: number = 1; // error TS1039: Initializers are not allowed in ambient contexts.

declare也允許出現在.ts文件中,但一般不會這麼做,.ts文件中直接用let/const/function/class就可以聲明並初始化一個變量。並且.ts文件編譯後也會去掉declare的語句,所以不需要declare語句。

註意,declare多個同名的變量是會沖突的

declare let foo: number; // error TS2451: Cannot redeclare block-scoped variable 'a'.

declare let foo: number; // error TS2451: Cannot redeclare block-scoped variable 'a'.

除瞭使用declare聲明一個值,declare還可以用來聲明一個模塊和全局的插件,這兩種用法都是在特定場景用來給第三方包做聲明。

declare module用來給一個第三方模進行類型聲明,比如有一個第三方包foo,沒有類型聲明。我們可以在我們項目中實現一個聲明文件來讓TypeScript可以識別模塊類型:foo.d.ts

// foo.d.ts
declare module 'foo' {
    export let size: number;
}

然後我們就可以使用瞭:

import foo from 'foo';

console.log(foo.size);

declare module除瞭可以用來給一個模塊聲明類型,還可以用來實現模塊插件的聲明。後面小節中會做介紹。

declare global用來給擴展全局的第三方包進行聲明,後面小節介紹。

模塊化

模塊語法

聲明文件的模塊化語法和.ts模塊的類似,在一些細節上稍有不同。.ts導出的是模塊(typescript會根據導出的模塊判斷類型),.d.ts導出的是類型的定義和聲明的值。

聲明文件可以導出類型,也可以導出值的聲明

// index.d.ts

// 導出值聲明
export let a: number;

// 導出類型
export interface Person {
    name: string;
};

聲明文件可以引入其他的聲明文件,甚至可以引入其他的.ts文件(因為.ts文件也可能導出類型)

// Person.d.ts
export default interface Person {name: string}

// index.d.ts
import Person from './person';

export let p: Person;

如果聲明文件不導出,默認是全局可以訪問的

// person.d.ts
interface Person {name: string}
declare let p: Person;

// index.ts
let p1: Person = {name: 'Sam'};
console.log(p);

如果使用模塊導出語法(ESM/CommJS/UMD),則不解析為全局(當然UMD還是可以全局訪問)。

// ESM

interface Person {name: string}

export let p: Person;

export default Person;
// CommonJS
interface Person {name: string}

declare let p: Person;

export = p;
// UMD
interface Person {name: string}

declare let p: Person;

export = p;
export as namespace p;

註意:UMD包export as namespace語法隻能在聲明文件中出現。

三斜線指令

聲明文件中的三斜線指令,用於控制編譯過程。

三斜線指令僅可放在包含它的文件的最頂端。

如果指定–noResove編譯選項,預編譯過程會忽略三斜線指令。

reference

reference指令用來表明聲明文件的依賴情況。

/// <reference path="…" />用來告訴編譯器依賴的其他聲明文件。編譯器預處理時候會將path指定的聲明文件加入進來。路徑是相對於文件自身的。引用不存在的文件或者引用自身,會報錯。

/// <reference types="node" />用來告訴編譯器它依賴node_modules/@types/node/index.d.ts。如果你的項目裡面依賴瞭@types中的某些聲明文件,那麼編譯後輸出的聲明文件中會自動加上這個指令,用以說明你的項目中的聲明文件依賴瞭@types中相關的聲明文件。

/// <reference no-default-lib="true"/>,

這涉及兩個編譯選項,–noLib,設置瞭這個編譯選項後,編譯器會忽略默認庫,默認庫是在安裝TypeScript時候自動引入的,這個文件包含 JavaScript 運行時(如window)以及 DOM 中存在各種常見的環境聲明。但是如果你的項目運行環境和基於標準瀏覽器運行時環境有很大不同,可能需要排除默認庫,一旦你排除瞭默認的 lib.d.ts 文件,你就可以在編譯上下文中包含一個命名相似的文件,TypeScript 將提取該文件進行類型檢查。

另一個編譯選項是–skipDefaultLibCheck這個選項會讓編譯器忽略包含瞭/// <reference no-default-lib="true"/>指令的聲明文件。你會註意到在默認庫的頂端都會有這個三斜線指令,因此如果采用瞭–skipDefaultLibCheck編譯選項,也同樣會忽略默認庫。

amd-module

amd-module相關指令用於控制打包到amd模塊的編譯過程

///<amd-module name='NamedModule'/>這個指令用於告訴編譯器給打包為AMD的模塊傳入模塊名(默認情況是匿名的)

///<amd-module name='NamedModule'/>
export class C {
}

編譯結果為

define("NamedModule", ["require", "exports"], function (require, exports) {
    var C = (function () {
        function C() {
        }
        return C;
    })();
    exports.C = C;
});

場景

這裡我們將自己的項目代碼稱為“內部項目”,引入的第三方模塊,包括npm引入的和script引入的,稱為“外部模塊”。

1. 在內部項目中給內部項目寫聲明文件

自己項目中,給自己的模塊寫聲明文件,例如多個模塊共享的類型,就可以寫一個聲明文件。這種場景通常不必要,一般是某個.ts文件導出聲明,其他模塊引用聲明。

2. 給第三方包寫聲明文件

給第三方包寫聲明文件又分為在內部項目中給第三方包寫聲明文件和在外部模塊中給外部模塊寫聲明文件。

在內部項目中給第三方包寫聲明文件: 如果第三方包沒有TS聲明文件,則為瞭保證使用第三方包時候能夠通過類型檢查,也為瞭安全地使用第三方包,需要在內部項目中寫第三方包的聲明文件。

在外部模塊中給外部模塊寫聲明文件: 如果你是第三方庫的作者,無論你是否使用TypeScript開發庫,都應該提供聲明文件以便用TypeScript開發的項目能夠更好地使用你的庫,那麼你就需要寫好你的聲明文件。

這兩種情況的聲明文件的語法類似,隻在個別聲明語法和文件的處理上有區別:

  • 內部項目給第三方包寫聲明文件時候,以.d.ts命名即可,然後在tsconfig.json中的files和include中配置能夠包含到文件即可,外部模塊的聲明文件需要打包到輸出目錄,並且在package.json中的type字段指定聲明文件位置;或者上傳到@types/<moduleName>中,使用者通過npm install @types/<moduleName>安裝聲明文件。redux就在tsconfig.json中指定瞭declarationDir為./types,TypeScript會將項目的聲明都打包到這個目錄下,目錄結構和源碼一樣,然後redux源碼入口處導出瞭所有的模塊,因此types目錄下也有一個入口的聲明文件index.d.ts,並且包含瞭所有的導出模塊聲明,redux在package.json中指定types字段(或者typings字段)為入口的聲明文件:./types/index.d.ts。這樣就實現瞭自動生成接口的聲明文件。
  • 內部項目給第三方寫聲明文件時候,如果是通過npm模塊引入方式,如import moduleName from 'path';則需要通過declare module '<moduleName>'語法來聲明模塊。而外部模塊的聲明文件都是正常的類型導出語法(如export default export =等),如果聲明文件在@types中,會將與模塊同名的聲明文件作為模塊的類型聲明;如果聲明文件在第三方包中,那麼就TypeScript模塊就將它作為這個第三方包模塊的模塊聲明,當使用者導入並使用這個模塊時候,TypeScript就根據相應地聲明文件進行類型提示和類型檢查。

根據第三方包類型可以分成幾種

全局變量的第三方庫

我們知道如果不使用模塊導出語法,聲明文件默認的聲明都是全局的。

declare namespace person {
    let name: string
}

或者

interface Person {
    name: string;
}

declare let person: Person;

使用:

console.log(person.name);

修改全局變量的模塊的第三方庫的聲明

如果有第三方包修改瞭一個全局模塊(這個第三方包是這個全局模塊的插件),這個第三方包的聲明文件根據全局模塊的聲明,有不同的聲明方式

如果全局模塊使用命名空間聲明

declare namespace person {
    let name: string
}

根據命名空間的聲明合並原理,插件模塊可以這樣聲明

declare namespace person {
  	// 擴展瞭age屬性
    let age: number;
}

如果全局模塊使用全局變量聲明

interface Person {
    name: string;
}

declare let person: Person;

根據接口的聲明合並原理,插件模塊可以這樣聲明

interface Person {
  	// 擴展瞭age屬性
    age: number;
}

上面的全局模塊的插件模塊的聲明方式可以應用於下面的場景:

  • 內部項目使用瞭插件,但插件沒有聲明文件,我們可以在內部項目中自己實現聲明文件。
  • 給插件模塊寫聲明文件並發佈到@types。

如果是插件模塊的作者,希望在項目中引用全局模塊並且將擴展的類型輸出到聲明文件,以便其他項目使用。可以這樣實現

// plugin/index.ts

// 註意這樣聲明才會讓TypeScript將類型輸出聲明文件
declare global {
  	// 假設全局模塊使用全局變量的方式聲明
    interface Person {
        age: number
    }
}

console.log(person.age);

export {};

註意,declare global寫在聲明文件中也可以,但是要在尾部加上export {}或者其他的模塊導出語句,否則會報錯。另外declare global在聲明文件中寫的話,編譯後不會輸出到聲明文件中。

修改window

window的類型是interface Window {…},在默認庫中聲明,如果要擴展window變量(如一些hybrid環境)可以這樣實現

// window.d.ts

// 聲明合並	
interface Window {
		bridge: {log(): void} 
}

// 或者
declare global {
    interface Window {
        bridge: {log(): void} 
    }
}

或者

// index.ts

declare global {
    interface Window {
        bridge: {log(): void} 
    }
}

window.bridge = {log() {}}

export {};

ESM和CommonJS

給第三方的ESM或者CommonJS模塊寫聲明文件,使用ESM導出或者CommonJS模塊語法導出都可以,不管第三方包是哪種模塊形式。

看下面示例

interface Person {
    name: string;
}

declare let person: Person;
 
export = person;
// 也可以使用export default person;
import person from 'person';

console.log(person.name);

上面的聲明文件是放在node_modules/@types/person/index.d.ts中,或者放在node_modules/person/package.json的types或者typings字段指定的位置。

如果在自己項目中聲明,應該使用declare module實現

declare module 'person' {
    export let name: string;
}

UMD

UMD模塊,在CommonJS聲明的基礎上加上export as namespace ModuleName;語句即可。

看下面的ESM的例子

// node_modules/@types/person/index.d.ts
interface Person {
    name: string;
}

declare let person: Person;

export default person;

export as namespace person;

可以通過import導入來訪問

// src/index.ts
import person from 'person';

console.log(person.name);

也可以通過全局訪問

// src/index.ts

// 註意如果用ESM導出,全局使用時候先訪問defalut屬性。
console.log(person.default.name);

下面是CommonJS的例子

// node_modules/@types/person/index.d.ts

interface Person {
    name: string;
}

declare let person: Person;

export default person;

export as namespace person;

可以通過import引入訪問

// src/index.ts

import person from 'person';

console.log(person.name);

也可以全局訪問

// src/index.ts

console.log(person.name);

模塊插件

上面我們提到,declare module不僅可以用於給一個第三方模塊聲明類型,還可以用來給第三方模塊的插件模塊聲明類型。

// types/moment-plugin/index.d.ts

// 如果moment定義為UMD,就不需要引入,直接能夠使用
import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

// src/index.ts

import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

比如作為redux的插件的redux-thunk的聲明文件extend-redux.d.ts,就是這樣聲明的

// node_modules/redux-thunk/extend-redux.d.ts

declare module 'redux' {
		// declaration code......
}

總結

到此這篇關於TypeScript聲明文件的語法與場景詳解的文章就介紹到這瞭,更多相關TS聲明文件內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: