TS 匯入匯出那些事

2023-04-08 06:01:17

前言

最近用 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 };
}

使用全域性定義

全域性定義一般有三種:

  1. 內建在 TS 中的全域性定義。比如 setTimeoutError 等。
    對於這種全域性定義,直接拿來用就可以了。

  2. 位於環境模組中的全域性定義。比如 NodeJS.Process 等。

    包括位於 node_modules/@types 資料夾中的自動引入的環境模組,都可以通過三斜槓註釋來引入。

    你可以通過 path 直接指定檔案路徑

    /// <reference path="./types.d.ts" />
    
  3. 位於模組中的全域性定義。

    這種全域性定義只需要引入一下模組,表示你已經執行此模組,即可

    import '@babel/core';
    

    或者你也可以通過三斜槓註釋,通過 types 指定模組

    /// <reference types="@babel/core" />
    

    需要注意的是,不論你採用 import 還是三斜槓註釋,甚至只是在型別中使用了一個 typeof import('xxx') ,只要你在一個 TS 檔案中引入了這個模組所定義的全域性型別,那這個型別就會永遠存在下去,汙染你的 globalThis
    唯一在不汙染全域性域的情況下執行模組的方法是使用 import() 函數動態引入,但這樣子你也拿不到你需要的型別。

進行全域性定義

進行全域性定義一般有三種方法。

第一種是直接寫環境模組。不帶任何 importexport 一般就會讓編譯器把這當成一個環境模組。所以,如果你需要防止一個 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')