昨天我們探討了宣告檔案的使用情境,知道如何載入第三方宣告檔案,以及如何判斷函式庫使用方式,今天要來探討如何撰寫宣告檔案.d.ts以及如何發布宣告檔案。
在使用情況不同的函式庫,宣告檔案撰寫的内容和方式會有所區別。
1. 全域函式庫(Global Libraries)、全域套件
常見全域宣告語法:
declare var/let/const 宣告全域變數(const為常數)
declare function 宣告全域函式
declare class 宣告全域類別
declare enum 宣告全域列舉型別
declare namespace 宣告(含有子屬性的)全域物件
interface 和 type 宣告全域類別
==declare var/let/const==
舉例來說:
src/jQuery.d.ts檔案
//將jQuery宣告為全域變數(let 和 var 沒什麼差別,一個是ES5語法,一個是ES6語法)
declare let jQuery: (selector: string) => any;
declare var jQuery: (selector: string) => any;
src/index.ts 檔案
//由於是全域變數宣告,不需導入即可在任何地方調用或修改
jQuery('#foo');
jQuery = function(selector) {
return document.querySelector(selector);
倘若是將jQuery宣告為全域常數 const
src/jQuery.d.ts檔案
//將jQuery宣告為全域常數
declare const jQuery: (selector: string) => any;
常數宣告後,就不允許修改了,如果修改就會報錯
src/index.ts 檔案
jQuery = function(selector) {
return document.querySelector(selector);
// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.
==declare function==
declare function 用來定義全域函式型別。全域函式也支援函式重載(function overloading),範例程式碼如下:
src/jQuery.d.ts檔案
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
src/index.ts 檔案
jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
==declare class==
declare class 用來定義類別,要注意的是裡面只能定義型別,不可以含有具體的執行程式碼
src/Animal.d.ts 檔案
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
callMyName() {
return `My name is ${this.name}`;
}; // ERROR: An implementation cannot be declared in ambient contexts.
src/index.ts 檔案
let cat = new Animal('Tom');
==declare enum==
和其他全域變數宣告一樣,declare enum 僅用來定義型別,而非具體的值。
src/Directions.d.ts
declare enum Directions {
Down,
Left,
Right
src/index.ts
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
==declare namespace==
前幾天在研究namespace時,其實對於namespace和模組的差異仍有些模糊。今天剛好看到一份資料較清楚說明兩者的差異,簡單來說,namespace可以看作是還沒有ES6模組出現的早期模組化方式,而後 ES6 出現了模組,TS就將早期的模組化方式改名為namespace,以兼容兩者。
現階段 TS 將每個檔案都視為獨立的模組,因此,現在幾乎很少使用 namespace 了,但在宣告文件中仍然會使用 declare namespace,用來表示有很多子屬性的全域物件變數。以jQuery例子來說,jQuery可以定義是一個全域物件變數,提供jQuery.ajax的方法供開發者使用,這時候可以使用 declare namespace jQuery 來宣告這個有多個屬性的全域變數。
src/jQuery.d.ts檔案
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
enum EventType {
CustomClick
src/index.ts 檔案
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
namespace裡面做嵌套,簡單來說,namespace裡面可以宣告另一個namespace,例如:
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
==interface/type==
interface和type不需要使用declare就可以在全域使用
src/jQuery.d.ts檔案
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
src/index.ts 檔案
let settings: AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
一般來說,應盡量減少全域變數 interface和 type,以減少可能的命名衝突,因此,通常會在外層使用 namespace 包起來。
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
function ajax(url: string, settings?: AjaxSettings): void;
在使用時,就會以jQuery.(運算符)來取得namespace下的屬性或方法
jQuery.ajax('/api/post_something', settings);
上面提到了jQuery宣告,有可能jQuery既是物件又是函式嗎? 答案是可以的。在 TS 中函式、類別和介面都可以多次宣告,它們會不衝突的合併起來。上面有提到的函式重載(function overloading)就是函式多次宣告。舉例來說:
interface Alarm {
price: number;
interface Alarm {
weight: number;
上面 Alarm 介面宣告了兩次,而裡面的屬性會合併。
同樣以jQury範例來說,是可以如此宣告的:
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
此時,jQuery既可以做全域函式使用,也可以命名空間的物件方式操作裡面的方法。
2. 模組函式庫(Module Libraries)
昨天有提到如何下載別人已寫好的模組函式庫的宣告文件,倘若找不到就得自己寫了,那檔案要放在哪裡呢?
假設有一個 foo 模組,推薦方式會是在跟 src 同層創建一個 types 資料夾,專門放置自己寫的模組宣告檔案,然後將 foo 的宣告檔案 foo.d.ts 放到 types/ 資料夾下,又或者是將 foo 的宣告檔案命名為index.d.ts,放到types/foo/的資料夾下,並搭配tsconfig.json 中的 paths 和 baseUrl 設定。
"compilerOptions": {
"baseUrl": "./",
"paths": {
"*": ["types/*"]
如此設定之後,透過 import 導入 foo 時,TS 編譯器也會去 types 資料夾下找尋相對應的模組宣告檔案了。至於 TS 查找檔案的順序請參考Day26文章,有更清楚的說明。上面的範例找尋檔案的順序如下:
/types/foo.d.ts
/types/foo/package.json (如果指定"types"屬性)
/types/foo/foo/index.d.ts
也就是說,如果types資料夾下有一foo.d.ts檔案,也有一個資料夾foo,裡面放置一個index.d.ts檔案,則 TS 編譯器會優先使用foo.d.ts檔案。
模組的宣告方式和全域宣告檔案不太一樣。在模組宣告檔案中,仍然會使用declare進行宣告,但使用declare只能在同一檔案中調用,除非在宣告檔案中使用 export 導出,其他檔案使用import導入後才可以使用。
常見模組導出語法:
export
導出變數
export default
預設導出
export =
commonjs 導出模組
==export 導出一或多個模組==
types/foo/index.d.ts 檔案
declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
export { name, getName, Animal };
==export default==
在 ES6 模組系統中,可以使用 export default 導出一個預設值,之後可以用 import foo from 'foo' 而非 import { foo } from 'foo' 來導入。
types/foo/index.d.ts 檔案
export default function foo(): string;
src/index.ts 檔案
import foo from 'foo';
foo();
要注意的是,只有函式、類別和介面才可以直接預設導出,其他型別需要使用declare宣告才能做預設導出,而且通常預設導出的語句會放在檔案的最上方。
//錯誤寫法
export default enum Directions {
Down,
Left,
Right // ERROR: Expression expected.
//正確寫法
export default Directions;
declare enum Directions {
Down,
Left,
Right
==export=(等於)==
使用 commonjs 規範的函式庫,我們會用下面方式來導出一個模組:
// 全部導出
module.exports = foo;
// 單一導出
exports.bar = bar;
而導入方式則有三種:
第一種方式是 const ... = require
// 全部導入
const foo = require('foo');
// 單一導入
const bar = require('foo').bar;
第二種方式是import ... from
// 全部導入
import * as foo from 'foo';
// 單一導入
import { bar } from 'foo';
第三種方式是import ... require(官方推薦使用)
// 全部導入
import foo = require('foo');
// 單一導入
import bar = foo.bar;
舉例來說:
types/foo/index.d.ts檔案
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
需要注意的是,使用export = 之後就不能在單一導出 export { bar } ,但可以透過混合宣告,使用 declare namespace foo 把 bar 放進 foo 裡。事實上,import ... require 和 export = 都是 TS 為了兼容 AMD 規範和 commonjs 規範而創建的新語法,並不常使用,也不推薦使用。
3. UMD 模組
可以透過 <script>
標籤導入,也可以使用 import
導入。TS提供了新語法 export as namespace
實踐。
一般使用 export as namespace 时,大部分是引入別人已經定義好的第三方宣告檔案,基於此再加上 export as namespace 語句,就可以將宣告好的一個變數變成全域變數。
==export as namespace==
一般使用 export as namespace 時都是函式庫已經有宣告檔案,再基於此宣告檔案下加上 export as namespace 語句,就可以將宣告好的變數變成全域變數。
types/foo/index.d.ts檔案
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
也可以和export default一起使用
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
4. 把模組擴展成全域
對於模組或UMD宣告檔案來說,只有export導出的型別宣告才能被導入,那如果希望宣告檔案能擴展成全域變數型別,就需要使用declare global
types/foo/index.d.ts檔案
declare global {
interface String {
prependHello(): string;
export {};
src/index.ts檔案
'bar'.prependHello();
要注意的是,即使宣告檔案不需要導出任何東西,仍然需要導出一個空物件,告訴編譯器這是模組宣告檔案而非全域變數宣告檔案。
5. 模組套件
有時透過import 導入一個模組套件,會改變原本模組的結構。假如模組套件沒有宣告檔案,就會導致型別不完整,TS 提供了語法declare module用來宣告擴展原本模組的型別。
==declare module==
要拓展原本模組,就需要先導入原本模組再進行拓展
types/moment-plugin/index.d.ts檔案
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();
declare module也可以用在同一個檔案中宣告多個模組
declare module 'foo' {
export interface Foo {
foo: string;
declare module 'bar' {
export function bar(): string;
發布宣告檔案
根據不同使用情境自行撰寫宣告檔案之後,下一步就是把檔案放在正確的地方讓編譯器找到並使用,編譯器會按下面的順序來查找:
在package.json檔中的 types 指定宣告檔案查找位置
根目錄下的index.d.ts
針對package.json中的main指定檔案下的同名不同後綴的.d.ts檔案
第一種方式:在package.json檔中設定types
"name": "foo",
"version": "1.0.0",
"main": "lib/index.js",
"types": "foo.d.ts",
指定了 types 为 foo.d.ts 之後,引入此函式庫時,就會去找 foo.d.ts 宣告檔案,如果没有指定 types ,就會從根目錄下查找 index.d.ts 檔案,如果還是沒有,編譯器就會去 main 下尋找 lib/index.d.ts檔案,如果還是沒有就會被認為沒有宣告檔案。
備註:如果是昨天提到的npm install別人寫好的宣告檔案,則檔案自動存在./node_modules/@types
中。
結合昨天探討的內容,先來總結一下撰寫宣告檔案的原則:
從型別角度來分:原始型別(string、number等)、物件型別、TS 擴充型別(複合型別、列舉、介面等)(可參考Day05型別總覽)。有些型別可以直接宣告導出例如:函式、類別和介面,有些則必須先使用 declare 宣告才可導出。
從程式碼 scope 來分:全域、模組或是既可以全域也可以模組(UMD),不同的scope會採取不同的宣告方式。
從檔案來源來分:自己撰寫.d.ts檔案和引用別人寫的.d.ts檔案。若為引用別人寫的宣告檔案,則直接使用 npm 下載並放置在node_modules/@types資料夾中; 若是自行撰寫的宣告檔案,則需放置在特定資料夾位置或在package.json設定檔中進行設定,編譯器才能找到該宣告檔案。