跳至主要内容

[TS] Namespaces and Modules

TL;DR

// namespace import
import * as yup form 'yup';

// namespace re-export
export * from 'types/device';
export * as foo from 'types/device';

// re-export: make default-export becomes to named-export
export { default } from 'components/Card';
export { default as ResidentCard } from './ResidentCard';

// 只 import type 而非 value
import type { HasPhoneNumber } from '../typescript-fundamental-v2/1-basics';

// make file as modules
export {};

此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料:

在 TypeScript 1.5 以後,原本的 internal modules 改名為 namespaces;原本的 external modules 改名為 modules

在 TypeScript 中,只要檔案中有使用到 importexport,則該檔案即會被視為 module;但若沒有,則會套用 TypeScript 中 namespaces 個概念,這將會導致不同檔案間同名稱的變數有衝突的情況。要解決這個問題,只需要在檔案的最上方加上 export {}; 讓這支檔案被當成 module 來處理即可

Modules

在 Modules 中可以同時包含程式碼(code)和宣告(declaration)。Modules 的使用需要依靠 module loader(例如,CommonJs/Require.js)或其他支援 ES Modules 的執行環境(runtime)。在當今的 Node.js 應用程式中,相較於 namespaces 的用法,modules 是更推薦的使用方式。

如同 ES6,在 TypeScript 中任何包含 importexport 的檔案都被視為 module,相反的若沒有使用到 importexport 則被視為 script,這裡面的變數會暴露在全域。

export interface StringValidator {
isAcceptable(s: string): boolean;
}

// 函式和 Class 的語法可以直接接在 `export default` 後而不需要特別命名。
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}

import type

在匯入型別(type)時,原本是使用 import,在 TypeScript 3.8 之後,還可以使用 import type 語法:

// Explicitly use import type
import type { APIResponseType } from './api';

// Re-using the same import
import { APIResponseType } from './api';

default exports

使用 export default 匯出:

// pkg.ts
const foo = 'foo';
export default foo;

使用 import <name> from <path> 匯入:

// main.ts
import bar from './pkg';
console.log(bar); // foo

export = 和 import = require() 語法

在 TS 中還有一種特殊的 export =import = require() 的用法,這兩種必須對應使用。這個用法通常適合用在下述的情況。

情境描述

由於 TypeScript 在預設情況下(esModuleInterop: false),會有以下假設:

import * as moment from 'moment';

// 等同於
const moment = require('moment');

import moment from 'moment';

// 等同於
const moment = require('moment').default;

此時如果 JavaScript 的 library 是直接匯出一個函式:

function getMockPerson() {
return { firstName: 'aaron', lastName: 'Chen' };
}

// CJS 對應的寫法:
// module.exports = getMockPerson;
export = getMockPerson; // TS 中對應的寫法

若我們想要使用這個 library 時,會發生錯誤:

// namespace import 載入的內容會是物件
import * as getMockPerson from './getMockPerson';

// 會試圖去拿 getMockPerson.default,但這並不存在
import getMockPerson from './getMockPerson';

namespace import

解決方式 1:使用 import = require() 語法

使用 export = 來匯出:

// getMockPerson.ts
function getMockPerson() {
return { firstName: 'aaron', lastName: 'Chen' };
}

// 等同於 CJS 中的:
// module.exports = getMockPerson;
export = getMockPerson;

使用 import <name> = require('<path>') 來匯入:

// main.ts
import getMockPerson = require('./getMockPerson');
console.log(getMockPerson());

解決方式 2:使用 esModuleInterop 的 flag

esModuleInterop @ TypeScript

若把 tsconfig.json 中的 esModuleInterop 設成 true,則在最後 bundle 出來的檔案就會多出對應的 helper 來處理上述的問題。

但要特別留意的是,如果你寫的是 TS Library 又把 esModuleInterop 設成 true 的話,等於後續使用你 library 的開發者也會必須要把這個 flag 打開才行。因此如果你寫的是 library 的話,不太建議把這個 flag 打開

warning

若開發的 library 把 esModuleInteropallowSyntheticDefaultImports 這兩個 flag 設成 true,則會迫使使用此 library 的人也要把這兩個 flag 設成 true。

動態載入(dynamic/optional module loading)

Optional Module Loading and Other Advanced Loading Scenarios @ TypeScript

重新匯出(re-exports)

使用 export * from <path> 可以直接把某一個模組重新匯出,而不會改變原有的內容(not mutate):

// foo.ts
export * from './ParseIntBasedZipCodeValidator';
export * as utilities from './utilities';

Ambient Modules

keywords: d.ts

撰寫第三方套件時,若想要讓 TypeScript 知道套件的樣貌,需要清楚定義套件所公開的 API,在 TypeScript 中,把這種只有定義而沒有實際操作內容的部分稱作 Ambient,通常會以 .d.ts 的副檔名作為結尾。

這裡可以使用 declare 搭配 module 這兩個關鍵字來完成,也就是 declare module "<module_name>"

// pkg.ts
declare module 'pkg' {
export interface SomeType {
foo: string;
bar: number;
}
export function greet(name: string): void;
}

正常匯入:

import * as PKG from 'pkg';

const foobar: PKG.SomeType = {
foo: 'foo',
bar: 2,
};

結構化 module 的建議

  • 以靠近最上層的方式匯出(exporting near the top-level),因為 modules 本身就已經有自己的 scope,因此盡可能不要有(用)額外的 namespaces,透過檔名本身就已經有分類的作用。
  • 如果只是要匯出「一個」 function 或 class,直接使用 export default
export default function greet() {
return 'Hello, TypeScript';
}
  • 如果需要匯出多個物件,將他們都放到最上層
export const foo = 'foo';

export function greet() {
return 'Hello, TypeScript';
}
  • 清楚列出所有匯入 module 的名稱:
import { foo, greet } from './pkg';
  • 如果需要一次匯入非常多東西,則是用 namespace 的方式來匯入
import * as myLargeModule from './MyLargeModule.ts';
// myLargeModule.foo

Namespaces

Namespaces 是 TypeScript 獨特組織程式碼的方式。它們其實就是在 global namespace 下的 JavaScript 物件。

Namespaces 通常是用來支援傳統 JS library 使用的,因為過去許多 library 並沒有使用 JS 原生 module 的方式在進行管理,而是將函式暴露在全域(例如,jQuery),這時候就可以透過 namespace 來定義 $ 的型別:

namespace $ {
export function ajax(arg: {
url: string;
data: any;
success: (response: any) => void;
}): Promise<any> {
return Promise.resolve();
}
}

一般來說,我們不需要花太多時間來了解 TS 中 namespaces 這個東西,因為它主要是用來針對傳統的 JS library 做向下相容用的。

處理 Assets:匯入不是 TS 的檔案

我們有時會需要匯入不是 TS 的檔案,例如 SVG、PNG、JPN 等等這類的 assets,這類檔案因為不是 TS 檔,也沒有提供對應的 type definition,因此 TypeScript compiler 會不高興:

define assets with TS

要解決這個問題,可以用上面提到的 namespace。

建立一支 assets.d.ts 的檔案:

// assets.d.ts
declare module '*.svg' {
const content: any;
export default content;
}

declare module '*.jpg' {
const content: string;
export default content;
}

declare module '*.png' {
const content: string;
export default content;
}

declare module '*.json' {
const content: string;
export default content;
}

記得 tsconfig 要用 include 到這隻檔案,如果沒有的話,要把這隻檔案 include 進來:

// tsconfig.json
{
"include": ["src", "assets.d.ts"]
}