昨天我們探討了宣告檔案的使用情境,知道如何載入第三方宣告檔案,以及如何判斷函式庫使用方式,今天要來探討如何撰寫宣告檔案.d.ts以及如何發布宣告檔案。
在使用情況不同的函式庫,宣告檔案撰寫的内容和方式會有所區別。
declare var/let/const 宣告全域變數(const為常數)
declare function 宣告全域函式
declare class 宣告全域類別
declare enum 宣告全域列舉型別
declare namespace 宣告(含有子屬性的)全域物件
interface 和 type 宣告全域類別
舉例來說:
//將jQuery宣告為全域變數(let 和 var 沒什麼差別,一個是ES5語法,一個是ES6語法)
declare let jQuery: (selector: string) => any;
declare var jQuery: (selector: string) => any;
//由於是全域變數宣告,不需導入即可在任何地方調用或修改
jQuery('#foo');
jQuery = function(selector) {
return document.querySelector(selector);
};
倘若是將jQuery宣告為全域常數 const
//將jQuery宣告為全域常數
declare const jQuery: (selector: string) => any;
常數宣告後,就不允許修改了,如果修改就會報錯
jQuery = function(selector) {
return document.querySelector(selector);
};
// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.
declare function 用來定義全域函式型別。全域函式也支援函式重載(function overloading),範例程式碼如下:
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
});
declare class 用來定義類別,要注意的是裡面只能定義型別,不可以含有具體的執行程式碼
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.
}
let cat = new Animal('Tom');
和其他全域變數宣告一樣,declare enum 僅用來定義型別,而非具體的值。
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
前幾天在研究namespace時,其實對於namespace和模組的差異仍有些模糊。今天剛好看到一份資料較清楚說明兩者的差異,簡單來說,namespace可以看作是還沒有ES6模組出現的早期模組化方式,而後 ES6 出現了模組,TS就將早期的模組化方式改名為namespace,以兼容兩者。
現階段 TS 將每個檔案都視為獨立的模組,因此,現在幾乎很少使用 namespace 了,但在宣告文件中仍然會使用 declare namespace,用來表示有很多子屬性的全域物件變數。以jQuery例子來說,jQuery可以定義是一個全域物件變數,提供jQuery.ajax的方法供開發者使用,這時候可以使用 declare namespace jQuery 來宣告這個有多個屬性的全域變數。
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
}
enum EventType {
CustomClick
}
}
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不需要使用declare就可以在全域使用
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
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既可以做全域函式使用,也可以命名空間的物件方式操作裡面的方法。
昨天有提到如何下載別人已寫好的模組函式庫的宣告文件,倘若找不到就得自己寫了,那檔案要放在哪裡呢?
假設有一個 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檔案,也有一個資料夾foo,裡面放置一個index.d.ts檔案,則 TS 編譯器會優先使用foo.d.ts檔案。
模組的宣告方式和全域宣告檔案不太一樣。在模組宣告檔案中,仍然會使用declare進行宣告,但使用declare只能在同一檔案中調用,除非在宣告檔案中使用 export 導出,其他檔案使用import導入後才可以使用。
export
導出變數export default
預設導出export =
commonjs 導出模組
declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
}
export { name, getName, Animal };
在 ES6 模組系統中,可以使用 export default 導出一個預設值,之後可以用 import foo from 'foo' 而非 import { foo } from 'foo' 來導入。
export default function foo(): string;
import foo from 'foo';
foo();
要注意的是,只有函式、類別和介面才可以直接預設導出,其他型別需要使用declare宣告才能做預設導出,而且通常預設導出的語句會放在檔案的最上方。
//錯誤寫法
export default enum Directions {
Up,
Down,
Left,
Right // ERROR: Expression expected.
}
//正確寫法
export default Directions;
declare enum Directions {
Up,
Down,
Left,
Right
}
使用 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;
舉例來說:
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 規範而創建的新語法,並不常使用,也不推薦使用。
可以透過 <script>
標籤導入,也可以使用 import
導入。TS提供了新語法 export as namespace
實踐。
一般使用 export as namespace 时,大部分是引入別人已經定義好的第三方宣告檔案,基於此再加上 export as namespace 語句,就可以將宣告好的一個變數變成全域變數。
一般使用 export as namespace 時都是函式庫已經有宣告檔案,再基於此宣告檔案下加上 export as namespace 語句,就可以將宣告好的變數變成全域變數。
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;
}
對於模組或UMD宣告檔案來說,只有export導出的型別宣告才能被導入,那如果希望宣告檔案能擴展成全域變數型別,就需要使用declare global
declare global {
interface String {
prependHello(): string;
}
}
export {};
'bar'.prependHello();
要注意的是,即使宣告檔案不需要導出任何東西,仍然需要導出一個空物件,告訴編譯器這是模組宣告檔案而非全域變數宣告檔案。
有時透過import 導入一個模組套件,會改變原本模組的結構。假如模組套件沒有宣告檔案,就會導致型別不完整,TS 提供了語法declare module用來宣告擴展原本模組的型別。
要拓展原本模組,就需要先導入原本模組再進行拓展
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
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;
}
根據不同使用情境自行撰寫宣告檔案之後,下一步就是把檔案放在正確的地方讓編譯器找到並使用,編譯器會按下面的順序來查找:
{
"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
中。
結合昨天探討的內容,先來總結一下撰寫宣告檔案的原則: