相关文章推荐
强悍的海龟  ·  jsx/tsx使用cssModule和typ ...·  1 周前    · 
虚心的豌豆  ·  TypeScript ...·  1 周前    · 
幸福的金针菇  ·  okhttp3 ...·  6 月前    · 
逃课的茶壶  ·  学习笔记 ...·  1 年前    · 

昨天我們探討了宣告檔案的使用情境,知道如何載入第三方宣告檔案,以及如何判斷函式庫使用方式,今天要來探討如何撰寫宣告檔案.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設定檔中進行設定,編譯器才能找到該宣告檔案。
  •