首頁 > 軟體

TypeScript宣告檔案的語法與場景詳解

2022-05-14 19:02:57

簡介

宣告檔案是以.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宣告檔案內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com