第一週第六天:宣告檔案與內建物件

第一週第六天:宣告檔案與內建物件

學習資源


什麼是宣告檔案(Declaration Files)?

宣告檔案是包含宣告語句的特殊 TypeScript 檔案,它們的檔案副檔名通常是 .d.ts。這些檔案用於定義 TypeScript 不認得的型別資訊(例如,第三方函式庫提供的全域變數),讓 TypeScript 編譯器在編譯時能夠識別這些類型,並且提供對應的程式碼自動完成、介面提示等功能。

什麼是宣告語句(Declaration Statements)?

在 TypeScript 中用於告訴 TypeScript 編譯器某個變量、函數或類的型別。它們主要用於描述 JavaScript 運行環境中已經存在的變量、函數或類的型別,以便 TypeScript 可以正確地檢查和使用它們。

以使用第三方函式庫 jQuery 為例

假如我們要使用 jQuery,一種常見的方式是在 HTML 中透過 <script> 標籤引入 jQuery,然後就可以使用全域變數 jQuery$ 了。但 jQuery 本身並不是用 TypeScript 編寫的,TypeScript 無法理解 jQuery 的型別資訊而報錯:

jQuery('#my-element');
// 錯誤:找不到名稱 'jQuery'。

這時我們就需要建立一個宣告檔案,如 jquery.d.ts,並且使用宣告語句 declare var 來定義它的型別,讓 TypeScript「看懂」jQuery 的型別:

// 宣告檔案 jquery.d.ts
declare var jQuery: (selector: string) => any;

使用宣告語句 declare var 並不會真的定義一個變數,只是定義一個全域變數 jQuery 的型別,僅用於編譯時的檢查,在編譯結果中會被刪除。它編譯的結果是這樣:

jQuery('#my-element');

另外要注意的是,宣告語句中只能定義型別,請勿在宣告語句定義具體的實作:

declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
// 錯誤:不得在環境內容中宣告實作

除了 declare var 之外,當然還有很多其他種宣告語句,後面會詳細介紹。

一般來說,TypeScript 會解析專案中所有的 *.ts 檔案,當然也包含 .d.ts 的檔案。所以當我們將宣告檔案 jquery.d.ts 放到專案中,其他所有 *.ts 檔案都可以取得用宣告語句定義的 jQuery 了。

假如仍然無法解析到 jQuery 的定義,可以檢查 tsconfig.json 中的 filesincludeexclude 設置,確保有包含到 jquery.d.ts 檔案。

通常,我們不需要手動撰寫這些宣告檔案,因為許多流行的第三方函式庫已經有對應的宣告檔案。可以使用 DefinitelyTyped 專案來找到對應的函式庫(jQuery 的在這)並安裝這些宣告檔案。對於 jQuery,我們可以使用 @types 來安裝宣告檔案:

npm install --save-dev @types/jquery

如何撰寫宣告檔案?

當第三方函式庫沒有提供宣告檔案時,我們就需要自己撰寫宣告檔案。而在不同情境下,宣告檔案的內容和使用方式會有所區別。

第三方函式庫的使用情境主要有以下幾種:

  • 全域變數:透過 <script> 標籤引入第三方函式庫,注入全域變數

  • npm 套件:透過像 import React from 'react' 形式匯入,符合 ES6 模組規範

  • UMD 函式庫:既可以透過 <script> 標籤引入,又可以透過 import 匯入

  • 直接擴充套件全域變數:透過 <script> 標籤引入後,改變一個全域變數的結構

  • 在 npm 套件或 UMD 函式庫中擴充套件:引用 npm 套件或 UMD 函式庫後,改變一個全域變數的結構或模組的結構

全域變數

在宣告檔案內宣告全域變數主要有以下幾種宣告語法:

  • declare var 宣告全域變數

  • declare function 宣告全域函式

  • declare class 宣告全域類別

  • declare enum 宣告全域列舉型別

  • declare namespace 宣告(含有子屬性的)全域物件

  • interfacetype 宣告全域型別

declare var

在所有宣告語句中,declare var 是最簡單的,如我們以 jQuery 所說的,它用來定義一個全域變數的型別。與其類似的還有 declare letdeclare const

declare let jQuery: (selector: string) => any;

但使用 declare const 定義時,表示此時的全域變數是一個常數,不允許再去修改它的值:

// 宣告檔案(例如 jquery.d.ts)
declare const jQuery: (selector: string) => any;

// 你的 TypeScript 程式碼
jQuery('#some-element').css('display', 'none');

// 如果你試圖修改 jQuery 的值,TypeScript 會報錯
jQuery = (selector: string) => { /* 另一個實作 */ }; // 錯誤:無法分配到 "jQuery",因為它是只讀屬性

一般來說全域變數都是禁止修改的常數,所以大部分情況都應該使用 declare const 而不是declare letdeclare var

declare function 宣告全域函式

declare function 用來定義全域函式的型別,例如 jQuery 其實就是一個函式,所以我們也可以用 declare function 來定義:

declare let jQuery: (selector: string) => any;

​也可以使用函式多載:

declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;

declare class 宣告全域類別

當全域變數是一個類別的時候,我們用 declare class 來定義它的型別:

// 宣告檔案(例如:global-class.d.ts)
declare class GlobalClass {
    constructor(name: string);
    getName(): string;
}

// 你的 TypeScript 程式碼
const globalInstance = new GlobalClass('myName');
console.log(globalInstance.getName());

declare enum 宣告全域列舉型別

declare enum 用於宣告列舉型別,也稱作外部列舉(Ambient Enums):

// 宣告檔案(例如:global-enum.d.ts)
declare enum Color {
    Red,
    Green,
    Blue
}

// 你的 TypeScript 程式碼
const myColor: Color = Color.Red;

declare namespace 宣告(含有子屬性的)全域物件

namespace 這個關鍵字是 TypeScript 早期還沒有 ES6 時,TypeScript 提供了一種模組化方案:使用 module 關鍵字來表示內部模組。但後來 ES6 也使用了 module 關鍵字,TypeScript 為了相容 ES6,另外使用 namespace (命名空間)來替代自己原本的 module 關鍵字。

隨著 ES6 的廣泛應用,現在推薦使用 ES6 的模組化方案,已經不建議使用 TypeScript 中的 namespace 。因此我們不再需要學習 namespace 的使用了。

雖然 namespace 被淘汰了,但在宣告檔案內 declare namespace 還是很常用來將一組相關的類型、函數、類別等組織在一個全域物件中。

例如 jQuery 是一個全域變數,它是一個物件,提供了一個 jQuery.ajax 方法可以呼叫,那麼我們就應該使用 declare namespace jQuery 來「裝」這個擁有多個子屬性的全域變數。

declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

這邊要注意的是,在 declare namespace 內部我們可以直接使用 function ajax 來宣告函式,而不是使用 declare function ajax。其他像是 constclassenum 等宣告語句也都可以直接使用:

declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    const version: number;
    class Event {
        blur(eventType: EventType): void
    }
    enum EventType {
        CustomClick
    }
}
巢狀命名空間

如果物件有很多層級,則可以使用在原本 namespace 底下再定義一個​ namespace 來宣告該個層級的屬性型別(巢狀):

// 原本的 namespace
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    // 再定義一個 namespace
    namespace fn {
        function extend(object: any): void;
    }
}

interfacetype 宣告全域型別

interface 是 TypeScript 的核心元素,用於定義型別的形狀。它可以描述物件的形狀、函數的形狀,甚至是類別的形狀。使用 interface 宣告全域型別時,可以在全域內使用該型別。

// 宣告檔案(例如:global-interface.d.ts)
interface GlobalInterface {
    name: string;
    age: number;
}

// 你的 TypeScript 程式碼
const person: GlobalInterface = {
    name: 'Alice',
    age: 30
};

type 是 TypeScript 中的另一種方式來定義型別。

// 宣告檔案(例如:global-type.d.ts)
type GreetingFunction = (name: string, age: number) => string;

// 你的 TypeScript 代碼
const greeting: GreetingFunction = (name, age) => {
  return `Hello, my name is ${name} and I am ${age} years old.`;
};

雖然書中提到 typeinterface 類似,但在研究官網和其他文章後,我發現這兩者其實存在一些差異。我打算之後再另外撰寫關於這部分的內容,以免讓這篇文章太長。

如何避免命名衝突?

當我們使用 interfacetype 宣告語句時,表示在全域內都可以使用該型別,這也同時表示每次新增型別時,命名需要避免跟原有的型別「撞名」。因此,我們可以用前面提過的命名空間(Namespace)來避免全域範圍中產生命名衝突:

// 宣告檔案(例如 jquery.d.ts)
declare namespace jQuery {
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}

// 你的 TypeScript 程式碼
// 使用 interface `AjaxSettings` 時需加上 namespace `jQuery`
let settings: jQuery.AjaxSettings = {
    method: 'POST',
    data: {
        name: 'foo'
    }
};
jQuery.ajax('/api/post_something', settings);
宣告合併

宣告合併是 TypeScript 的一個特性,允許我們將多個獨立的宣告合併為單個宣告。這在擴展現有類別、接口、模組等時非常有用。

例如 jQuery 既是一個函式,又是一個物件,我們就可以使用相同的名稱 jQuery 來宣告,TypeScript 會將這些宣告合併成一個 jQuery 的定義:

// 宣告檔案(例如 jquery.d.ts)
declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

// 你的 TypeScript 程式碼
jQuery('#my-element');
jQuery.ajax('/api/get_something');

關於宣告合併,我們會在後續章節中看到更詳細的說明,這邊就先不深究。

npm 套件

先查看該 npm 套件是否已經有現成寫好的宣告檔案:

  1. npm 套件已綁定宣告檔案
    可以看 package.json 中是否有 types 欄位,或者有一個 index.d.ts 的宣告檔案。這個情況是最方便的,因為我們不需要額外安裝其他套件。這也提醒我們在建立套件時,最好也把宣告檔案與 npm 套件綁定。

  2. @types 已有宣告檔案

    我們可以透過嘗試安裝相應的 @types 套件來檢查是否存在對應的宣告檔案。例如,想要確認 jQuery 是否有宣告檔案,可以使用安裝指令 npm install --save-dev @types/jquery 。如果安裝成功,即表示有對應的宣告檔案;若無法安裝成功,則代表沒有相對應的宣告檔案。

如果前述兩種方法都無法找到相應的宣告檔案,那麼我們就需要自行撰寫對應的宣告檔案。

由於 npm 套件是透過 import 匯入模組,因此宣告檔案放哪裡就有其限制。通常會將它放在專案的根目錄下的 typestypings 資料夾內,接著,在此目錄中為該 npm 套件建立一個 .d.ts 檔案。以下是一個示範目錄結構:

your_project/
├── src/
│   └── ...
├── types/
│   └── some_package/
│       └── index.d.ts
└── tsconfig.json

接下來,在 tsconfig.json 中設定 typeRootspaths,以便 TypeScript 編譯器能找到您撰寫的宣告檔案。以下是 tsconfig.json 的配置:

{
  "compilerOptions": {
    ...
    "typeRoots": ["./types", "./node_modules/@types"],
    ...
  },
  ...
}

或者,使用 paths

{
  "compilerOptions": {
    ...
    "baseUrl": ".",
    "paths": {
      "some_package": ["./types/some_package/index.d.ts"]
    },
    ...
  },
  ...
}

當我們寫好套件的宣告檔案後,別忘了提交檔案到第三方函式庫或 @types 來分享與回饋開源社群喔!

npm 套件的宣告檔案通常針對模組化的程式碼,使用 ES 模組或 CommonJS 模組等模組系統。主要有以下幾種宣告語句來匯出模組中的類型、函式等:

  • export 匯出變數

  • export namespace 匯出(含有子屬性的)物件

  • export default ES6 預設匯出

  • export = CommonJS 匯出模組

export

npm 套件的宣告檔案與全域變數的宣告檔案有很大的區別。在 npm 套件的宣告檔案中,使用 declare 並不會宣告一個全域變數,只會在當前的檔案中宣告一個區域性變數。只有在宣告檔案中使用 export 匯出,然後使用 import 匯入時才能使用這些型別宣告。

// 宣告檔案 my-package.d.ts
export function myFunction(param: string): void;
export interface MyInterface {
  property: string;
}
// 使用套件的檔案 main.ts
import { myFunction, MyInterface } from 'my-package';

// 使用 myFunction
myFunction('Hello, world!');

// 使用 MyInterface 定義一個物件
const myObject: MyInterface = {
  property: 'This is a property',
};

console.log(myObject.property);
結合 declare export

我們也可以先使用 declare 宣告多個變數,最後再用 export 一次匯出變數。

// 宣告檔案 my-package.d.ts
declare function myFunction(param: string): void;

// 注意:與全域變數的宣告檔案類似, `interface` 前是不需要加 `declare` 的
interface MyInterface {
    property: string;
}

export { myFunction, MyInterface };

export namespace

declare namespace 類似,export namespace 用來匯出一個擁有子屬性的物件:

// 宣告檔案 namespace.ts
export namespace MyNamespace {
  export const value = 42;
  export function myFunction(): void {
    console.log('This is myFunction');
  }
}
// 使用套件的檔案 main.ts
import { MyNamespace } from './namespace';

console.log(MyNamespace.value); // 輸出:42
MyNamespace.myFunction(); // 輸出:This is myFunction

export default

在 ES6 的模組系統中,使用 export default 指定一個匯出的預設值,直接來看下面的例子:

// 宣告檔案 defaultExport.ts
const myDefaultExport = 'Default export value';

export default myDefaultExport;
// 使用套件的檔案 main.ts
// 不是用 import { myValue } from './defaultExport';
import myValue from './defaultExport';

console.log(myValue); // 輸出:Default export value

只有 functionclassinterface 可以直接預設匯出,其他的變數需要先用宣告語句 declare 定義後才能用 export default 匯出:

// exportNumber.d.ts
// 🔴 錯誤寫法
export default const myNumber: number;

// 🟢 正確寫法
declare const myNumber: number;
export default myNumber;

一般我們會將預設匯出語句放在整個宣告檔案的最上方:

// exportNumber.d.ts
export default myNumber;

declare const myNumber: number;

export =

在 CommonJS 模組系統規範中,我們會用以下方式來匯出一個模組:

// myModule.js
function foo() {
  return 'foo';
}

foo.bar = 42;

// 全部匯出
module.exports = foo;

// 各別匯出
exports.bar = bar;

假如要為 CommonJS 規範的函式庫寫宣告檔案的話,就需要使用 export = 這種語法:

// 宣告檔案 myModule.d.ts
export = foo;

// 使用宣告合併,將 `bar` 合併至 `foo` 裡
declare function foo(): string;
declare namespace foo {
    const bar: number;
}

我們可以使用不同方式匯入此模組。

第一種方式是 const ... = require(...)

// 全部匯入
const foo = require('./myModule');

// 各別匯入
const bar = require('./myModule').bar;

第二種方式是 import ... from ...

注意:在 TypeScript 中,使用 ES6 模組語法與 CommonJS 模組不完全相容。需要確保在 tsconfig.json 中設定 "esModuleInterop": true,才能使用以下語法進行匯入。

// 全部匯入
import * as foo from './myModule';

// 各別匯入(不適用於使用 `export =` 的情況)
// import { bar } from './myModule'; // 這個語法無法使用

第三種方式是 import ... = require(...) ,也是 TypeScript 官方推薦的方式:

// 全部匯入
import foo = require('./myModule');

// 各別匯入
const bar = foo.bar;

準確來說,export = 不僅可以用在宣告檔案中,也可以用在一般 TypeScript 檔案中。

實際上 import ... requireexport = 都是 TypeScript 為了相容 AMD(Asynchronous Module Definition)和 CommonJS 規範而創立的新語法。由於不常用也不推薦使用,詳細介紹可以參考官方文件

由於現在很多第三方函式庫是 CommonJS 規範,所以我們才需要學如何使用 require ... importexport = 這些語法。但相較起來更推薦使用 ES6 標準的 export defaultexport

UMD 函式庫

既可以透過 <script> 標籤引入,又可以透過 import 匯入的函式庫,稱為 UMD(Universal Module Definition)函式庫。

除了匯出 npm 套件的宣告檔案,我們還需要額外宣告一個全域變數,因此 TypeScript 提供了一個新語法 export as namespace 來同時支援全域變數和模組匯入。

// 宣告檔案 myModule.d.ts
// 宣告變數 `foo` 在全域都可以用
export as namespace foo;
export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

也可以跟 export default 一起使用:

// 宣告檔案 myModule.d.ts
export as namespace foo;
export default foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

直接擴充套件全域變數

有時第三方函式庫新增一個全域變數,但這個全域變數的型別卻還沒有更新,就會導致 TypeScript 編譯錯誤。這時,我們就得自行擴充套件全域變數的型別。

例如我們可以用宣告合併,來擴充內建套件 String 型別:

// string-extension.d.ts
interface String {
  greet(): void;
}

接下來,我們需要在實作檔案中添加這個新增的方法:

// string-extension.ts
String.prototype.greet = function () {
  console.log(`Hello, ${this}!`);
};

現在,所有的字串都包含了原始的方法以及新增的方法 greet

// usage.ts
import './string-extension';

const myString = 'World';
myString.greet(); // Output: Hello, World!

我們也可以使用 declare namespace 來將新的型別宣告添加到第三方套件的命名空間中(意思等同擴充原本的第三方套件型別)。首先,在宣告檔案中定義新增的 jQuery 套件型別:

// types/jquery-plugin/index.d.ts
declare namespace JQuery {
  interface CustomOptions {
    bar: string;
  }
}

interface JQueryStatic {
  foo(options: JQuery.CustomOptions): string;
}

接著,我們需要在實作檔案中添加這個 jQuery 套件:

// jquery-plugin.js
(function ($) {
  $.foo = function (options) {
    return 'Hello, ' + options.bar;
  };
})(jQuery);

現在,我們可以在 TypeScript 中使用這個套件:

// usage.ts
import 'jquery';
import 'jquery-plugin';

const options: JQuery.CustomOptions = {
  bar: 'World',
};

console.log($.foo(options)); // Output: Hello, World

在 npm 套件或 UMD 函式庫中擴充套件

想要擴充 npm 套件或 UMD 函式庫時,通常會使用擴充宣告(augmentation declaration)來達到目的。這是因為這些函式庫通常會匯出特定的模組(module),而不是直接在全域中定義變數。在這種情況下,我們需要使用特定的語法來擴充模組中的型別。

declare global

這個語法用於擴充全域範圍,通常在我們需要為一個直接添加變數到全域範圍的 JavaScript 函式庫(例如,舊的 jQuery 套件)提供型別宣告時使用。在宣告檔案中使用 declare global,我們可以在全域添加新的型別、變數、函數等。但在現代 JavaScript 函式庫中,這種情況越來越少見,因為模組化的設計變得更為流行。

假設我們有一個名為 my-global-plugin 的 JavaScript 函式庫,它在全域中添加了一個名為 myGlobalFunction 的函數。現在,我們希望為這個函數新增 TypeScript 型別。

首先,我們需要在宣告檔案中使用 declare global 語法來擴充全域:

// my-global-plugin.d.ts
declare global {
  interface MyGlobalFunction {
    (): string;
    myProperty: number;
  }

  const myGlobalFunction: MyGlobalFunction;
}

在這個宣告檔案中,我們使用 declare global 語法來擴充全域。我們為 myGlobalFunction 定義了一個型別 MyGlobalFunction,並將該型別的定義添加到全域中。

這樣,在 TypeScript 中使用 my-global-plugin 時,myGlobalFunction 函數將具有正確的型別,並包含 myProperty 屬性。

declare module

這個語法用於為模組化的 JavaScript 函式庫(例如,大部分 npm 套件)提供型別宣告。當我們需要為一個模組化的函式庫提供型別宣告時,我們可以使用 declare module 語法。在宣告檔案中使用 declare module,我們可以為模組添加型別、變數、函數等。在現代 JavaScript 函式庫中,這是一種更常見的情況。

假設我們希望為 Moment.js 添加一個名為 myCustomMethod 的擴充方法,我們可以使用 declare module 來擴充 moment 模組。以下是一個範例:

// 宣告檔案 moment-extensions.d.ts
// 告訴 TypeScript 想擴充現有的 `moment`模組
declare module 'moment' {
  interface Moment {
    myCustomMethod(): string;
  }
}

接下來,我們需要在 Moment.js 的原型上實際實現 myCustomMethod 方法:

// moment-extensions.js
const moment = require('moment');

moment.fn.myCustomMethod = function () {
  return 'Hello from my custom method!';
};

現在,當我們在 TypeScript 專案中匯入 Moment.js 和這個擴充時,便可以正常使用 myCustomMethod

import moment from 'moment';
import './moment-extensions';

const now = moment();
console.log(now.myCustomMethod()); // 輸出 "Hello from my custom method!"

在不同的宣告檔案之間建立依賴關係

依賴關係其實就是指在一個宣告檔案會匯入另一個宣告檔案的型別,兩個宣告檔案之間會互相依賴。可以透過下列幾種方式來實現:

  1. 使用模組化的匯入 import

     // dependency.d.ts
     export interface MyInterface {
       foo: string;
     }
    
     // main.d.ts
     import { MyInterface } from './dependency';
    
     declare function myFunction(param: MyInterface): void;
    
  2. 使用三斜線指令 /// <reference ... />

    namespace 類似,三斜線指令也是 TypeScript 在早期版本中為了描述模組之間的依賴關係而創建的指令。隨著 ES6 的廣泛應用,現在已經不再建議使用 TypeScript 中的三斜線指令。但在宣告檔案中,少數情況我們需要使用三斜線指令而非 import

    • 當我們在撰寫一個全域變數的宣告檔案時

    • 當我們需要依賴一個全域變數的宣告檔案時

撰寫一個全域變數的宣告檔案

在這種情況下,我們要為一個全域變數(例如在瀏覽器環境下的 window 或其他全域物件)撰寫宣告檔案。這時,我們無法使用 import 關鍵字,因為 import 主要用於模組之間的依賴關係。我們需要改用三斜線指令:

// types/jquery-plugin/index.d.ts/// <reference types="jquery" />
declare function foo(options: JQuery.AjaxSettings): string; // 可以使用 JQuery.AjaxSettings 型別了

三斜線指令在 /// 後方使用 XML 的格式添加對 jquery 型別的依賴關係。另外需要注意的是,三斜線指令必須放在檔案的最頂端,指令前面指允許出現單行 /... 或多行註解 /*...*/

依賴一個全域變數的宣告檔案

在另個情況下,我們可能需要在一個宣告檔案中使用另一個宣告檔案中定義的全域變數。

例如,假設有兩個宣告檔案:

// globalVarA.d.ts
declare var globalVarA: number;

// globalVarB.d.ts
/// <reference path="globalVarA.d.ts" />
declare var globalVarB: typeof globalVarA;

在上面的例子中,globalVarB.d.ts 文件依賴 globalVarA.d.ts 文件,因此我們使用三斜線指令引用了 globalVarA.d.ts

拆分宣告檔案

當我們全域變數的宣告檔案太大時,可以透過拆分成多個檔案(像 React 拆元件一樣),然後集中在一個入口檔案中將它們逐一匯入,提高程式碼的可維護性。例如 jQuery 的宣告檔案就是這樣的:

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

/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />

export = jQuery;

可以看到裡面用到 typespath 兩種不同的指令。兩者的區別是:types 用來宣告對另一個函式庫的依賴,而 path 用來宣告對另一個檔案的依賴。

除了以上這兩種指令,還有其他的三斜線指令,例如 /// <reference no-default-lib="true"/>/// <amd-module /> 等,但它們都已廢棄,詳情請見官方網站

自動產生宣告檔案

如果函式庫的原始碼本身就是由 TypeScript 寫的,那麼在使用 tsc 指令將 TypeScript 編譯成 JavaScript 時,新增 declaration 的選項就可以同步產生 .d.ts 宣告檔案了。

我們可以在編譯指令中新增 --declaration 或簡寫 -d,或是在 tsconfig.json 中新增 declaration 選項。這裡以 tsconfig.json 為例:

{
    "compilerOptions": {
        "module": "commonjs",
        "outDir": "dist",
        "declaration": true, // 👈
    }
}

執行 tsc 指令後,目錄結構如下:

your_project/
├── dist 👈 outDir
│   └── bar
│       └── index.d.ts 👈
│       └── index.js
│   └── main.d.ts 👈
│   └── main.js
├── src/
│   └── bar
│       └── index.ts
│   └── main.ts
├── package.json
└── tsconfig.json

可以看到目錄結構內,src 目錄下有兩個 .ts 檔案,分別是 src/main.tssrc/bar/index.ts,它們的編譯結果被放在 dist 內的同時,也會產生對應的兩個宣告檔案 dist/main.d.tssrc/bar/index.d.ts。它們檔案的內容分別是:

// src/main.ts
export * from './bar';
​
export default function foo() {
    return 'foo';
}
// src/bar/index.ts

export function bar() {
    return 'bar';
}

自動產生的宣告檔案:


// dist/main.d.ts

export * from './bar';
export default function foo(): string;
// dist/bar/index.d.ts

export declare function bar(): string;

可以看到自動產生的宣告檔案,僅保持原始碼基本的結構,而將具體的實作去掉,生成對應的型別宣告。

除了 declaration 選項之外,還有幾個選項也與自動產生宣告檔案有關,這裡簡單列舉不作詳細說明:

  • declarationDir 設定產生 .d.ts 檔案的目錄

  • declarationMap 對每個 .d.ts 檔案,都產生對應的 .d.ts.map(sourcemap)檔案

  • emitDeclarationOnly 僅產生 .d.ts 檔案,不產生 .js 檔案

其實書中還有一個「如何發佈宣告檔案讓別人使用」的部份,但讀到這我已經對宣告檔案有點膩了(看看那超短的 Scrollbar,就知道宣告檔案篇幅有多長!),只好請有興趣的人移駕到這裡繼續閱讀,倒地舉白旗的安娜感謝各位的配合。之後甩掉油膩感後再回來補齊。

什麼是內建物件?

TypeScript 的內建物件是指在 TypeScript 中預先定義的一組基本物件和類型。它們主要是基於 JavaScript 的標準內建物件以及一些其他常見的全域物件。它們是 TypeScript 語言的一部分,無需額外安裝或引入即可使用。

ECMAScript 的內建物件

常見的 ECMAScript 內建物件包括:BooleanObjectArrayFunctionDateRegExpMathErrorTypeErrorRangeError 等,更多內建物件可以參考 MDN 的文件。它們的定義檔案則在 TypeScript 核心函式庫的定義檔案中。

const boolValue: boolean = Boolean(1); // true
const errorValue: Error = new Error('This is a custom error message.');
const currentDate: Date = new Date();
const regex: RegExp = /\d+/g;

DOM 和 BOM 的內建物件

DOM(文件物件模型)和 BOM(瀏覽器物件模型)的內建物件主要是用於瀏覽器環境中的。它們提供了操作網頁元素和瀏覽器功能的 API。它們的定義檔案同樣在 TypeScript 核心函式庫的定義檔案中。以下是一些常見的 DOM 和 BOM 內建物件:

  • DOM:DocumentElementNodeNodeListHTMLElement 等。

  • BOM:WindowLocationNavigatorScreenHistory 等。

// HTML 中的按鈕元素
// <button id="myButton">Click me</button>

const button = document.getElementById("myButton") as HTMLButtonElement;

button.addEventListener("click", () => {
  alert("Button clicked!");
});

console.log(window.location.href); // 輸出當前網址

什麼是 TypeScript 核心函式庫的定義檔案?

TypeScript 核心函式庫的定義檔案是 TypeScript 提供的一組預先定義所有瀏覽器環境需要用到的型別宣告檔案,用於描述 ECMAScript 標準、DOM API、BOM API 等常用的JavaScript 環境中的全域物件、函數和類型。

當我們安裝 TypeScript 時,核心函式庫定義檔案已經包含在其中。我們不需要做任何額外操作就可以使用它們。以下是在 TypeScript 中使用 ArrayPromise 的範例:

// 使用 Array 型別和方法
const numbers: number[] = [1, 2, 3];
const doubledNumbers = numbers.map((num) => num * 2);

// 使用 Promise 型別和方法
const fetchData = (): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
};

fetchData().then((data) => console.log(data));

用 TypeScript 寫 Node.js

TypeScript 核心函式庫的定義中並不包含 Node.js 的部份,所以如果想用 TypeScript 寫 Node.js,則需要另外安裝 Node.js 的型別宣告檔案。可以透過安裝 @types/node 套件來完成:

npm install --save-dev @types/node

安裝後,我們就可以在 TypeScript 檔案中使用 Node.js 的 API,例如 fs(檔案系統)模組、http(HTTP 伺服器)模組等。TypeScript 會使用 @types/node 提供的型別宣告檔案來對我們的程式碼進行型別檢查。