別人的輪子用起來
等等這輪子的規格好像怪怪的...
───────────────────── By Opshell
今天我們要蓋網站,一般來說都會使用很多套件,不常自己造輪子,
但是你要用別人的輪子,TypeScript看不懂,會哀哀叫。
所以使用套件時,我們需要宣告他的型別(說明書),
才能獲得對應的程式碼自動完成、介面提示等功能。
一般我們配合
ES6 模組
規範,用import foo from 'foo'
匯入一個npm套件,
在宣告套件型別前,可以看看需要先看看它的宣告檔案
是否已經存在。
什麼是宣告檔案
? 你可以想像成輪子的使用說明書,
你要把說明書給TS,他才看得懂:
套件的
宣告檔案
可能存在於兩個地方:
- 與該套件繫結在一起:
package.json
中有types
欄位,或者有一個index.d.ts
檔案。
這種模式不需要額外設定或安裝其他東西,是最方便的。
- 釋出到
@types
裡。我們可以嘗試安裝一下對應的@types
包,
或者在這邊搜尋看看,就知道是否存在該宣告檔案。
安裝命令是:
yarn add @types/foo -D。
這種模式一般是由於套件的維護者沒有提供
宣告檔案
,
所以由其他大神將宣告檔案
共享到@types
裡。
※ 當然也可以找到野生的說明書,可以直接下載來用,
但是套件一多會很亂,所以還是推薦使用@types
來管理。
宣告檔案(說明書)
如果上面兩種方式都找不到輪子,那大概就要自己做了,
自己幫套件做型別宣告有幾種方式,
先來講解做說明書會用到的大概語法:
declare(全域宣告)
用常用到的
jQuery
來做說明,
一般在JS使用都是這樣:
$(function(){ /** 巴拉巴拉... */});
但是 TypeScript 看不懂 $ 或 jQuery阿
這時候就可以用
declare(全域宣告)
把'$'宣告成全域變數,
和一般的JavaScrip需告變數一樣,只是在前面加上declare
,
而特性也差不多,let
、var
是變數const
是常數:
declare const $: (selector: any) => any;
$(function () { /** 巴拉巴拉... */ });
※
declare(宣告全域)
並沒有真的定義一個變數或方法,
只是定義了全域變數 $ 的型別,只會用在TypeScript
的檢查,在編譯後不會存在。
※ 由於上面的特性,所以declare(全域宣告)
並不能實作任何具體的功能,否則會報錯。
declare 可以用的包括
function
、class
、enum
、namespace
等。
而declare(全域宣告)
中有一些比較特別的是:
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 };
除了
declare(宣告全域)
的變數之外,可能有一些型別我們也希望能全域使用。
在型別宣告中,我們可以直接使用interface
或type
來宣告一個全域的介面或型別:
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
type
用法和interface
差不多,這邊就不細說囉。
※ 不過全域的interface
和type
容易有命名衝突的問題,
所以更好的做法是放進namespace(命名空間)
裡面處理:
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
當你在
declare(全域宣告)
型別時,重複的名稱並不會產生衝突,
而是會合併起來:
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
jQuery('#foo');
jQuery.ajax('/api/get_something');
一般我們會把
宣告語句
單獨放到.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
裡,files
、include
和exclude
的設定,是否包含了 jQuery.d.ts 的所在目錄。
像 Ops 習慣把.d.ts
檔案塞在一起,就需要像下面這樣設定一下tsconfig.json
。
/project
├── ts
| ├── index.ts
| └── types
| └── jQuery.d.ts
└── tsconfig.json
宣告的用意就是和TS介紹這些他不認識的人(套件)
這些人有什麼專長、特點,
介紹的越詳細,TS遇到他的時候就注意更多細節,
所以一些功能很複雜的套件,使用大老們處理好的,
省事到極點,說明書
還是用別人的方便省力,
但總有自己做輪子(套件)
的時候,這時候要附上說明書
,
明天的Prat 2 就來學學怎麼做個輪子使用說明書~
我想請教一下,我自己寫了一個宣告檔案,但是我無論怎麼設定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
還煩請解惑了,謝謝。
namespace 裡面的 type ,
要 export 喔,
declare namespace Resource {
export type resource = {}
}
但是現在越來越少用namespace了,
你可以參考TS 官網,
https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html
最下面提到的,
直接把namespace弄成單檔。
原來如此,明白了,剛開始在使用namespace的時候也看到許多文檔都說,現在不太使用namespace,由於自己是剛開始接觸TS,所以不是太明白其中道理,現在經Opshell大解答後,確定是得在宣告的型別前面,加上export變成模塊的方式使用了,謝謝你!!