iT邦幫忙

2022 iThome 鐵人賽

DAY 20
1
Modern Web

如何用TypeScript水30天鐵人賽系列 第 20

[Day20]:別人的輪子用起來 - 宣告檔案 Part 1

  • 分享至 

  • xImage
  •  

Day20 banner

用別人的輪子

別人的輪子用起來
等等這輪子的規格好像怪怪的...
───────────────────── By Opshell


目標:檔案宣告

今天我們要蓋網站,一般來說都會使用很多套件,不常自己造輪子,
但是你要用別人的輪子,TypeScript看不懂,會哀哀叫。
所以使用套件時,我們需要宣告他的型別(說明書),
才能獲得對應的程式碼自動完成、介面提示等功能。


過程

  • 用大神的說明書

    一般我們配合ES6 模組規範,用import foo from 'foo' 匯入一個npm套件,
    在宣告套件型別前,可以看看需要先看看它的宣告檔案是否已經存在。
    什麼是宣告檔案? 你可以想像成輪子的使用說明書,
    你要把說明書給TS,他才看得懂:

    套件的宣告檔案可能存在於兩個地方:

    1. 與該套件繫結在一起:
      package.json中有types欄位,或者有一個index.d.ts檔案。
      這種模式不需要額外設定或安裝其他東西,是最方便的。
    1. 釋出到@types裡。我們可以嘗試安裝一下對應的@types包,
      或者在這邊搜尋看看,就知道是否存在該宣告檔案。
      安裝命令是:
     yarn add @types/foo -D。
    

    這種模式一般是由於套件的維護者沒有提供宣告檔案
    所以由其他大神將宣告檔案共享到@types裡。

    ※ 當然也可以找到野生的說明書,可以直接下載來用,
    但是套件一多會很亂,所以還是推薦使用@types來管理。


  • 自己做宣告檔案(說明書)

    如果上面兩種方式都找不到輪子,那大概就要自己做了,
    自己幫套件做型別宣告有幾種方式,
    先來講解做說明書會用到的大概語法:

  • 1. declare(全域宣告)

    用常用到的jQuery來做說明,
    一般在JS使用都是這樣:

     $(function(){ /** 巴拉巴拉... */});
    

    但是 TypeScript 看不懂 $ 或 jQuery阿

    jQuery 錯誤

    這時候就可以用declare(全域宣告)把'$'宣告成全域變數,
    和一般的JavaScrip需告變數一樣,只是在前面加上declare
    而特性也差不多,letvar是變數const是常數:

     declare const $: (selector: any) => any;
    
     $(function () { /** 巴拉巴拉... */ });
    

    宣告全域變數

    declare(宣告全域) 並沒有真的定義一個變數或方法,
    只是定義了全域變數 $ 的型別,只會用在TypeScript的檢查,在編譯後不會存在。
    ※ 由於上面的特性,所以declare(全域宣告)並不能實作任何具體的功能,否則會報錯。

    declare 可以用的包括 functionclassenumnamespace等。
    declare(全域宣告)中有一些比較特別的是:

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

    以前沒有ES6 module的時候 TS 提供的模組化方案,
    現在有了ES6 module後改稱為 TS namespace(命名空間) 來使用,
    由於ES6 module的普及,現在幾乎不使用TS namespace(命名空間)來做模組化,
    但是還是很常拿來全域宣告成物件,因為它可以包含很多子屬性,使用上很方便:

     declare namespace Member {
       interface Info {
          title: string;
          age: number;
       }
    
       function getSummary(title: string, age: number): string;
     }
    

    以此類推,上面提到的各種declare(宣告全域)都可以放進來用,要省略掉declare。
    ※ 巢狀命名空間
    如果裡面有更深的層級會變成這樣:

     declare namespace Member {
       interface Info {
          title: string;
          age: number;
       }
    
       namespace Team {
          function getTeam(): Array<string>;
       }
    
       function getSummary(title: string, age: number): string;
     }
    
     // 使用長這樣 namespace名稱 + 子屬性名稱
     let Opshell: Member.Info = { title: 'Opshell', age: 30 };
    

    • 1-2. interface 和 type

    除了declare(宣告全域)的變數之外,可能有一些型別我們也希望能全域使用。
    在型別宣告中,我們可以直接使用interfacetype來宣告一個全域的介面或型別:

     interface AjaxSettings {
       method?: 'GET' | 'POST'
       data?: any;
     }
    
     declare namespace jQuery {
       function ajax(url: string, settings?: AjaxSettings): void;
     }
    

    type用法和interface差不多,這邊就不細說囉。
    ※ 不過全域的interfacetype容易有命名衝突的問題,
    所以更好的做法是放進namespace(命名空間)裡面處理:

     declare namespace jQuery {
       interface AjaxSettings {
          method?: 'GET' | 'POST'
          data?: any;
       }
    
       function ajax(url: string, settings?: AjaxSettings): void;
     }
    

    • 1-3. 宣告合併

    當你在declare(全域宣告)型別時,重複的名稱並不會產生衝突,
    而是會合併起來:

     declare function jQuery(selector: string): any;
     declare namespace jQuery {
       function ajax(url: string, settings?: any): void;
     }
    
     jQuery('#foo');
     jQuery.ajax('/api/get_something');
    

  • 2. 宣告檔案

    一般我們會把宣告語句單獨放到.d.ts的檔案裡面,
    所以.d.ts就是所謂的宣告檔案

     // ts/types/jQuery.d.ts
     declare const $: (selector: any) => any;
     declare function jQuery(selector: string): any;
     declare namespace jQuery {
       function ajax(url: string, settings?: any): void;
     }
    

    TS 會解析專案中所有的 *.ts 檔案,當然也包含以.d.ts結尾的檔案。
    所以將 jQuery.d.ts 放到專案裡(和.ts檔放在一起),
    所有*.ts檔案就都可以獲得 jQuery 的型別定義了。
    如果無法解析,那麼可以檢查 tsconfig.json 裡,
    filesincludeexclude的設定,是否包含了 jQuery.d.ts 的所在目錄。
    像 Ops 習慣把 .d.ts 檔案塞在一起,就需要像下面這樣設定一下tsconfig.json

     /project
     ├── ts
     |  ├── index.ts
     |  └── types
     |       └── jQuery.d.ts
     └── tsconfig.json
    

    宣告檔案編譯


小結:

宣告的用意就是和TS介紹這些他不認識的人(套件)
這些人有什麼專長、特點,
介紹的越詳細,TS遇到他的時候就注意更多細節,
所以一些功能很複雜的套件,使用大老們處理好的,
省事到極點,說明書還是用別人的方便省力,
但總有自己做輪子(套件)的時候,這時候要附上說明書
明天的Prat 2 就來學學怎麼做個輪子使用說明書~


上一篇
[Day19]:紅燈停綠燈行 - 型別檢測&Narrowing
下一篇
[Day21]:自己做說明書 - 宣告檔案 part 2
系列文
如何用TypeScript水30天鐵人賽33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Skywalker
iT邦新手 5 級 ‧ 2022-12-13 00:48:03

我想請教一下,我自己寫了一個宣告檔案,但是我無論怎麼設定include內的內容,TS還是一直要我用import引入的方式才能使用我宣告的那個namespace,請問我是哪個部分有錯誤了嗎?
(ps. 我的想法是,declare 一個全域變數後,若要使用該型別,則不需要另外寫import去引入,就能夠直接使用)

fetchData.d.ts

import { MyResponse } from "./helpers/FetchData" // 別的地方匯入的一個型別

declare namespace Resource {
  type resource = {
    resource: {
      result: {
        read: () => MyResponse // 上方匯入的型別供此處使用
      }
    }
  }
}

tsconfig.json

ts設定檔中的include裡,之前寫了"src/fetchData.d.ts"或"fetchData.d.ts"都沒有用,其他想使用該型別的組件都吃不到這個型別。

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "noImplicitOverride": true,
  },
  "include": [
    "src"
  ]
}

不知道是一定要在想使用型別的組件中,用import { Resource } from "../fetchData",像是這樣的方式引入才能用嗎?
我看到這個網站跟你說的內容很相似
https://willh.gitbook.io/typescript-tutorial/basics/declaration-files#declare-namespace
但是程式碼的部分都沒有寫到需要用import之類的方式引入,也查了很多其他資料也都沒有人提到...所以來這邊問問看QQ
還煩請解惑了,謝謝。

Opshell iT邦新手 4 級 ‧ 2022-12-13 17:11:12 檢舉

namespace 裡面的 type ,
要 export 喔,

  declare namespace Resource {
      export type resource = {}
  }

但是現在越來越少用namespace了,
你可以參考TS 官網,
https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html
最下面提到的,
直接把namespace弄成單檔。

Skywalker iT邦新手 5 級 ‧ 2022-12-14 22:33:09 檢舉

原來如此,明白了,剛開始在使用namespace的時候也看到許多文檔都說,現在不太使用namespace,由於自己是剛開始接觸TS,所以不是太明白其中道理,現在經Opshell大解答後,確定是得在宣告的型別前面,加上export變成模塊的方式使用了,謝謝你!!

我要留言

立即登入留言