模块、命名空间与 .d.ts 声明文件

本节目标

TS 项目里 import 时,TS 会去「找类型」:优先找 .ts,没找到就找同名 .d.ts。

// 运行环境:Node.js 16+ + typescript;类型检查 npx tsc --noEmit ts-l14.ts

// 正常导入带类型的模块
import { sum } from './math' // 若 math.ts 导出 sum:number,这里就有提示

// 给「没有类型的第三方 JS」补一个声明文件:math.d.ts
// 内容示例(放在被导入文件同目录或 @types 下):
// declare function legacyAdd(a: number, b: number): number
// declare module 'legacy-lib' { export function legacyAdd(a: number, b: number): number }

.d.ts 是「只有类型、没有实现」的文件,专门描述「某个 JS 长什么样」,让 TS 能给无类型库补上提示。

// 自己写的最简声明:告诉 TS 全局有个 formatDate 函数
declare function formatDate(ts: number): string
// 之后任何地方调用 formatDate 都有类型检查(实现由运行时 JS 提供)

模块 vs 命名空间:现代 TS 一律用 ES Module(import/export);namespace 是老写法,新项目几乎不用,别碰。

名词解释

声明文件(.d.ts):一种「只写类型、不写逻辑」的特殊文件。它像是给某个 JS 库的「说明书」:告诉 TS「这个库导出了什么、各长啥类型」。有了它,你 import 一个纯 JS 库也能享受类型提示。社区把常用库的声明集中放在 @types/* 包里(如 @types/lodash)。

模块(Module / ESM):用 import/export 把代码拆成「可复用、作用域隔离」的小文件。TS 原生支持 ES Module,import 时自动解析类型。它是现代前端组织的基石,替代了老式的「全局变量」和 namespace。

课后练习

练习 1:一个 JS 库只导出 function slugify(s){...},怎么给它写 .d.ts?

答案:新建 slugify.d.ts,写 declare function slugify(s: string): string; export = slugify;(或 export default function slugify(s: string): string)。之后 TS 就能识别它。

练习 2:TS 导入一个模块时,类型解析的优先级是怎样的?

答案:先找同名的 .ts 实现文件;找不到再找同名 .d.ts 声明;若是第三方包,还会查 node_modules/@types/<包名> 和包内 types 字段指向的 .d.ts。都不存在就报「找不到模块/类型」。

总结

我的看法:.d.ts 是 TS 生态能「兼容整个 JS 世界」的秘密武器——它让你既能用海量无类型 JS 库,又不丢了类型安全。新手最容易怕它,其实最小声明只要 declare 几行就能救命。关于 namespace,我的建议是新代码一律不写:它是 ES Module 普及前的过渡方案,现在用 import/export 既标准又清爽,工具链支持也最好。最后提醒:装第三方库时顺手 npm i -D @types/xxx 几乎成了肌肉记忆——类型提示的体验差距,用过就回不去。