TS 系列最後一篇。前面把型別系統與窄化打通了,這篇收尾在工程面:怎麼跨檔案 import / export、怎麼讓沒有型別的 JS 套件變得型別安全、怎麼擴充全域與第三方型別,以及寫 library 時該怎麼對外暴露 .d.ts。
ESM 為主
現代 TS 一律以 ES Module 為主。判斷標準很簡單:一個檔案只要有 import 或 export,就是模組,有自己的 scope;都沒有就是 script,跟其他 script 共享全域。
// math.ts
export function add(a: number, b: number) { return a + b }
export const PI = 3.14159
export default class Calculator {}
// 重新命名匯出
export { add as plus }
// index.ts
import Calc, { add, PI } from './math'
import { add as plus } from './math'
import * as math from './math'
export default 算是歷史包袱,多數團隊偏好 named export——IDE 重構友善,也避免同一個東西在不同檔案被取不同名字。
type-only import / export
只拿來標型別、執行期根本不需要存在的東西,加 type 標明:
import type { User } from './models'
import { type User, getUser } from './models' // 混合:getUser 是值,User 是型別
export type { User }
好處有三個:
tsc編譯時會把 type-only import 完全擦掉,不留import語句- 不會發生「只是要個型別,卻意外觸發那個模組的 runtime side effect」
- 有些 build 工具(esbuild、
isolatedModules模式)本來就依賴明確的 type import
建議開 verbatimModuleSyntax: true,強制所有純型別匯入都加 type。為什麼要強制?因為純型別 import 不加 type 的話,某些編譯工具(esbuild、swc、bundler)會誤把它當值 import 保留下來,等到執行期找不到那個符號就炸。開了之後規則變得很乾淨:import { T } 一律當值 import、import type { T } 一律編譯期消失,沒有灰色地帶。
路徑與 moduleResolution
moduleResolution 常用的就兩個值:
| 值 | 適用 |
|---|---|
bundler | 給 Vite / esbuild / webpack 等打包器(不要求副檔名) |
nodenext | 給 Node 直接跑(必須加 .js 副檔名,即使原始檔是 .ts) |
Node 模式下:
// 在 .ts 裡也要寫 .js
import { add } from './math.js'
在 .ts 裡寫 .js 看起來很怪,但理由是這樣:Node 的 ESM loader 只認 .js(或顯式 .mjs),完全不知道 TS 的存在;而 TS 編譯時又不會改寫 import 路徑字串。所以路徑必須預先寫成編譯後實際存在的檔名——這是「TS 不做路徑改寫」這個設計的必然結果。
路徑別名(paths)
在 tsconfig.json 設定:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}
}
import Button from '@/components/Button'
注意 TS 只負責型別檢查層的解析,執行期還要靠打包工具或額外 loader(例如 tsx、tsconfig-paths,Vite 則自帶支援)才真的跑得起來。
宣告檔(.d.ts)
.d.ts 只含型別宣告、沒有實作。tsc 編譯 .ts 時可以自動產生對應的 .d.ts(要開 declaration: true)。
// math.d.ts
export declare function add(a: number, b: number): number
export declare const PI: number
declare 的意思是:「這東西在執行期會存在,我只是跟編譯器報告它的型別」。
主要用在三個地方:
- 寫 library 時對外暴露 API 型別
- 給沒有型別的 JS 套件補型別
- 擴充全域型別
給 JS 套件補型別
假設 legacy-lib 是個沒型別的 JS 套件,自己幫它補一份。
src/types/legacy-lib.d.ts:
declare module 'legacy-lib' {
export function init(opts: { debug?: boolean }): void
export const version: string
}
確認 tsconfig.json 的 include 有涵蓋 src/types,之後就能型別安全地 import:
import { init, version } from 'legacy-lib'
如果不在意型別細節,也有偷懶寫法:
declare module 'some-untyped-lib'
// 整個套件當 any
ambient module(萬用宣告)
要 import 非 JS / TS 的資源(圖片、CSS)時,也要先告訴 TS 那是什麼型別,配合打包工具用:
// images.d.ts
declare module '*.png' {
const src: string
export default src
}
declare module '*.svg?component' {
import type { SvelteComponent } from 'svelte'
const C: SvelteComponent
export default C
}
import logo from './logo.png' // logo: string
import Icon from './icon.svg?component'
擴充全域 / 第三方型別
靠 interface 合併(前面 interface 那篇提過)就能擴充既有的型別。擴充全域:
// global.d.ts
declare global {
interface Window {
myApp: {
version: string
}
}
}
export {} // 必須有 export 才會被當成模組,declare global 才有效
window.myApp.version // ✅ 有型別
擴充第三方也是同一招,例如幫 Express 的 Request 加欄位:
// express.d.ts
import 'express'
declare module 'express' {
interface Request {
user?: { id: number; name: string }
}
}
三斜線指令(很少用,知道有就好)
/// <reference types="node" />
/// <reference path="./other.d.ts" />
這是舊式做法,現代 TS 已經用 tsconfig 的 types / typeRoots 設定取代。
tsconfig 與型別來源
{
"compilerOptions": {
"types": ["node", "jest"], // 只載入這些 @types/*,其餘都不載
"typeRoots": ["./node_modules/@types", "./src/types"]
}
}
不寫 types 時,TS 預設載入 node_modules/@types/* 底下的所有型別,可能混進不想要的東西。
寫 library 的 export 設計
package.json 之於 TS library,就像 Cargo.toml 之於 Rust。要對外提供型別,至少要有這些設定。
package.json:
{
"name": "my-lib",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
tsconfig.json:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "./dist"
}
}
exports 欄位讓 Node 與打包工具能正確對應到型別與實作。
一個專案常見的型別結構
src/
├── index.ts
├── lib/
│ └── client.ts
├── models/
│ └── user.ts // export interface User {}
└── types/
├── env.d.ts // declare namespace NodeJS { interface ProcessEnv {} }
├── images.d.ts
└── global.d.ts
把 ambient declarations(沒有 export 的 *.d.ts)集中放在 types/ 資料夾,好找也好管理。
速查
| 場景 | 工具 |
|---|---|
| 模組對外公開值 | export ... |
| 模組對外公開型別 | export type ... |
| 純型別匯入 | import type ... |
| 沒型別的 JS 套件 | declare module 'name' {} |
| 非 JS 資源(圖片、CSS) | declare module '*.ext' |
| 擴充全域 | declare global { interface Window {} } |
| 擴充第三方型別 | 在第三方模組 declaration merging |
TS 系列到此告一段落。八篇下來——環境、基本型別、interface/type、函式、泛型、utility types、窄化、模組——剛好是寫真實 TS 程式所需的最小完整地圖。後續的 decorator、TS plugin、各框架的延伸都是分支,之後有需要再展開。
Latest Updates
- 2026.06.11 Content updated
