模块、声明文件与第三方库当你开始把 TypeScript 真正放进项目里就会很快遇到一些不再是语法层面的现实问题代码和类型应该如何跨文件组织第三方库没有类型时怎么办为什么有些包能直接提示类型有些却报“找不到声明文件”.d.ts到底是什么它和普通.ts文件有什么关系这些问题不解决TypeScript 很难真正变成工程工具。因为类型系统的价值不只是写在单个文件里更在于它如何穿过模块边界、跨越包依赖、与外部世界协作。模块首先是 JavaScript 概念其次才是 TypeScript 概念这是一个特别值得先立住的认知。很多人学 TypeScript 时会把“模块系统”也当成 TS 独有内容其实不是。现代 TypeScript 默认建立在 JavaScript 的 ES Module 体系上exportinterfaceUser{id:number;name:string;}exportfunctiongetUser(id:number):User{return{id,name:Alice};}在另一个文件中import{getUser}from./user;TypeScript 在这里做的不是重新发明一套模块系统而是在已有模块系统上增加类型理解能力。为什么“模块边界”在 TypeScript 里格外重要因为模块边界通常意味着这些问题某个函数对外暴露了什么契约某个类型是否应该被外部消费某个模块内部实现细节是否不该泄露某个公共模型应该放在哪里才能避免循环依赖和重复定义你如果只会写单文件 TypeScript离工程实践其实还差很远。真正的项目质量很大程度上取决于模块边界是否清晰。import type是一个很值得养成的习惯importtype{User}from./user;这行代码的意思是这里只导入类型不导入运行时代码。为什么这值得强调因为它能帮助你在两个层面上变得更清楚语义层面这个导入只服务类型不参与运行时逻辑工程层面有助于工具链区分哪些依赖只存在于编译阶段在大型项目里import type和普通import的区分会让代码边界更清晰。.d.ts声明文件到底是什么声明文件通常以.d.ts结尾。它的作用不是提供实现而是告诉 TypeScript 某段运行时代码在类型层面长什么样。例如declaremodulemy-lib{exportfunctionformat(value:string):string;}这里并没有真正实现format只是告诉编译器“有这么一个模块它导出了这样一个函数请你以后按这个类型理解它。”你可以把声明文件理解成“给类型系统看的说明书”。为什么第三方库有时能直接用类型有时不行通常有三种来源库本身自带类型声明社区提供types/xxx你自己补.d.ts过去很多 JavaScript 库不自带类型所以你需要安装类似npminstall-Dtypes/lodash但现在很多现代库已经直接内置类型例如不少 React、Node.js、工具链生态里的主流包都不再需要单独装types。如何判断一个库有没有自带类型通常可以看包的package.json是否包含types或typings编辑器是否能直接识别类型npm 页面或文档是否说明内置 TypeScript 支持如果没有那再考虑是否存在types/xxx是否需要自己手写最小声明手写最小声明是很实用的工程技能并不是只有做库开发的人才会碰.d.ts。现实项目里你经常会遇到内部老模块没有类型一个很小的第三方包没人维护类型你临时接入了某个 JS 工具这时你完全可以先写一个最小声明文件满足当前使用需求declaremodulelegacy-lib{exportfunctionparse(input:string):{code:number;message:string;};}你不一定一开始就要把所有 API 都写全。很多时候只为当前真正使用到的部分补类型就已经足够让工程质量提升一个层级。声明文件的目标不是绝对完整而是逐步降低未知区域这是一个很现实的工程视角。很多人看到.d.ts会紧张好像必须一次性把整个库完整建模。其实不必。更实用的做法通常是先覆盖你当前用到的 API尽量避免any但不要过度投入在一次性完美建模上随着使用范围扩展再逐步补全这比一开始为了“完整性”花很多时间更符合项目现实。模块增强和全局声明要谨慎使用你还会遇到两类更进阶的声明能力。模块增强给已有模块补充额外类型declaremodulemy-lib{interfaceOptions{retry?:number;}}全局声明给window或其他全局对象补字段declareglobal{interfaceWindow{APP_VERSION:string;}}这些能力很强但越强的能力越容易被滥用。全局污染越多系统边界就越模糊。因此能局部声明时尽量不要上升到全局。为什么编辑器会提示“找不到模块声明”这是很多人踩过的坑。通常不是 TypeScript 本身出了问题而是下面这些环节之一没对上包没装类型没装导入路径写错tsconfig的模块解析配置不匹配声明文件没被编译器包含库的导出方式和你的导入方式不匹配也就是说遇到这类问题时优先排查工程配置和包结构不要只盯着代码行本身。一个工程上的好习惯把“运行时代码”和“类型边界”一起设计成熟的 TypeScript 项目不会把类型当作后补丁。它会在设计模块时一起考虑对外暴露哪些类型哪些类型只在模块内部使用对第三方依赖的类型信任程度是多少是否需要对外部数据再做运行时校验你越早把这些问题纳入设计后面的模块关系就越稳。本文小结模块解决的是代码和能力的组织方式声明文件解决的是“运行时存在、类型系统却不了解”的那部分鸿沟而第三方库类型则是 TypeScript 工程实践中绕不开的日常工作。你不一定每天都写.d.ts但你必须理解它在整个系统中的位置它是连接外部世界与类型系统的桥梁。一旦你理解了这层关系TypeScript 对你来说就不再只是单文件里的类型标注工具而会真正变成一个可以跨模块、跨包、跨边界运行的工程系统。练习把一个接口和函数拆到单独模块中再导入并尝试使用import type区分类型导入和普通导入。为一个没有类型的假想库手写一个最小声明文件只覆盖你会用到的 API。在浏览器项目里为window增加一个自定义字段声明并思考为什么全局扩展应该谨慎使用。后记2026年5月22日于上海。