最近用 TypeScript 寫 npm 包,各種模組、名稱空間、全域性定義等等擾得我睡不著覺。
我便苦心研究,總結了幾個比較冷門的,國內貌似基本上找不到資料的匯入匯出用法,順便在其中又插入一些不那麼冷門的用法,於是本篇文章來了。
因為一開始也沒想做成大全,可能之後還會繼續更新吧。
匯入模組中的東西相信大家都不陌生。唯一需要注意的便是預設匯出與「星號」匯出的區別。
import * as Mod from './mod';
// 類似於
const Mod = require('./mod');
import ModDef from './mod';
// 類似於
const { default: ModDef } = require('./mod');
import ModDef, { a, b } from './mod';
// 類似於
const {
default: ModDef,
a, b
} = require('./mod');
在模組中匯出東西相信大家也不陌生,不過這裡還是詳細講解一下。
在模組中匯出東西有很多種方法。匯出總共可分為 4 類:
命名匯出有兩種方法,一種是宣告著匯出
export namespace A { }
export function b() { }
export class C { }
export const d = 123;
export let { e } = { e: 'hh' };
一種是宣告後匯出
namespace A { }
function b() { }
class C { }
const d = 123;
let { e } = { e: 'hh' };
export { A, b, C, d, e };
宣告後匯出比宣告著匯出更靈活,能合併,也能重新命名
namespace A { }
export { A };
function b() { }
export { b as c };
class C { }
export { C as B };
const d = 123;
let { e } = { e: 'hh' };
export { d, e };
命名匯出編譯成 Common JS 後類似這樣
exports.xxx = xxx;
需要注意的是其他人無法修改任何你匯出的東西。即使是使用 let
宣告也一樣
/* mod.ts */
export let a = 123;
/* others.ts */
import { a } from './mod';
a = 321; // 報錯:ts(2632)
不過對於上面的程式碼,你可以隨便修改所匯出的 a
。因為其他人每次讀取 a
時都會重新從你的匯出物件上存取一次 a
屬性,不用擔心其他人無法接收到你的修改。具體可以檢視編譯後的 JS 檔案
/* others.ts */
import { a } from './mod';
const b = a + 123;
console.log(a);
/* others.js */
var mod_1 = require("./mod");
var b = mod_1.a + 123;
console.log(mod_1.a);
預設匯出可以理解為一種特殊的命名匯出。
預設匯出的名字是 default
。但是你不能搞個名字叫 default
的變數然後匯出,你必須得用 export default
或者在匯出時重新命名
export let default = 123; // 報錯:ts(1389)
export default 123; // 正確
export let a = 123;
export { a as default }; // 正確
星號匯入搭配預設匯出,可以達到預設匯出即為星號匯出的效果
/* mod.ts */
import * as Self from './mod';
export default Self;
/* others.ts */
import * as Mod from './mod';
import ModDef from './mod';
console.log(Mod === ModDef); // true
星號匯出可以匯出其他模組裡的東西。一般有兩種用法。
第一種是全匯出到自己裡頭,就像
export * from './xxx'
具體效果是 xxx
中匯出的東西,也可以通過你存取到。
/* xxx.ts */
export let a = { hh: 'hh' };
/* mod.ts */
export * from './xxx.ts';
/* others.ts */
import { a } from './xxx';
import { a } from './mod';
console.log(a === a); // true
第二種是掛到自己模組下面,就像
export * as xxx from './xxx';
// 等價於
import * as xxx from './xxx';
export { xxx };
匯出分配就是把 Common JS 的匯出搬到了 TS 中。寫法也差不多
export = 'hh';
// 相當於
module.export = 'hh';
匯出分配也可以指定預設匯出,只需要有 default
屬性就可以
/* mod.ts */
export = { default: 123 };
/* others.ts */
import mod from './mod';
console.log(mod); // 123
需要注意的是採用了匯出分配後便不能再使用其他匯出方法。
雖然現在名稱空間相較於模組並不是特別常用,但它還是有比較完整的匯入匯出功能的。
匯入名稱空間中的東西很簡單,像這樣
import xxx = Space.xxx;
不論你是在模組中匯入全域性名稱空間,還是在名稱空間中匯入其他名稱空間,都是適用的。
import Err = globalThis.Error;
throw Err('hh');
namespace A {
import Process = NodeJS.Process;
let proce: Process;
}
較為可惜的是名稱空間貌似沒有星號匯入,也不支援解構匯入。
在一般 TS 中,名稱空間只有兩種方法匯出。
第一種方法是宣告著匯出,類似於模組
namespace A {
export const a = 123;
}
第二種方法是匯入著匯出,可以用來匯出其他名稱空間的東西
namespace A {
export import Err = globalThis.Error;
}
而對於不一般的 TS ——也就是型別宣告中,名稱空間還可以採用像模組一樣的匯出物件
declare namespace A {
const a = 123;
const b = 'hh';
export { a, b };
}
全域性定義一般有三種:
內建在 TS 中的全域性定義。比如 setTimeout
、 Error
等。
對於這種全域性定義,直接拿來用就可以了。
位於環境模組中的全域性定義。比如 NodeJS.Process
等。
包括位於 node_modules/@types
資料夾中的自動引入的環境模組,都可以通過三斜槓註釋來引入。
你可以通過 path
直接指定檔案路徑
/// <reference path="./types.d.ts" />
位於模組中的全域性定義。
這種全域性定義只需要引入一下模組,表示你已經執行此模組,即可
import '@babel/core';
或者你也可以通過三斜槓註釋,通過 types
指定模組
/// <reference types="@babel/core" />
需要注意的是,不論你採用 import
還是三斜槓註釋,甚至只是在型別中使用了一個 typeof import('xxx')
,只要你在一個 TS 檔案中引入了這個模組所定義的全域性型別,那這個型別就會永遠存在下去,汙染你的 globalThis
。
唯一在不汙染全域性域的情況下執行模組的方法是使用 import()
函數動態引入,但這樣子你也拿不到你需要的型別。
進行全域性定義一般有三種方法。
第一種是直接寫環境模組。不帶任何 import
和 export
一般就會讓編譯器把這當成一個環境模組。所以,如果你需要防止一個 TS 檔案變成環境模組導致型別洩露的話,你可以加一個安全無公害的 export { };
。
第二種是在模組中定義,只需要把型別定義寫到 declare global
裡頭就行
declare global {
const a = 123;
let b: {};
}
a; // 123
b; // {}
第三種是通過合併 globalThis
名稱空間來定義,好處是可以使用名稱空間的「匯入著匯出」方法,將模組或者其他名稱空間的區域性變數變成全域性變數
import _Mod from './mod';
declare global {
namespace globalThis {
const a = 123;
export import Mod = _Mod;
}
}
a; // 123
Mod; // typeof import('./mod')