TypeScript编译报错TS2307 Cannot find module与TS2339 Property does not exist on type的排查方法(moduleResolution、@types类型包与tsconfig配置定位)

升级完依赖再跑构建,控制台一下刷出几十行TS2307;改了一行再普通不过的对象取属性,又冒出TS2339。这两个报错在TypeScript项目里出现频率最高,成因却完全不同:TS2307是模块解析阶段压根没找到这个模块,TS2339是模块找到了但类型上没有这个成员。先分清是哪一种,后面处理起来快得多。

两种报错的完整形态长这样:

error TS2307: Cannot find module '@/utils/request' or its corresponding type declarations.
error TS2339: Property 'userInfo' does not exist on type 'AxiosResponse<any, any>'.

TS2307后半句那个or its corresponding type declarations值得留意,它说明问题可能出在模块本身不存在,也可能出在模块存在但缺.d.ts声明文件。这一个后缀就把排查方向劈成了两半。

先确认报错是谁报的

编辑器画的红线和命令行跑出来的结果经常对不上。遇到过好几次:VS Code里一片红,npx tsc --noEmit却是0错误,或者正好反过来。原因多半是编辑器用的是它自带的TypeScript版本,而项目node_modules里装的是另一个。

命令行执行npx tsc --version看一眼,再到编辑器命令面板执行TypeScript: Select TypeScript Version,选Use Workspace Version。两边版本没对上之前,别急着改代码。

TypeScript 5.7、5.8、5.9、6.0,这几个版本在模块解析和默认配置上都有调整,老项目升上来之后大面积爆TS2307,基本都出在这一段。

TS2307的四种成因

第一种最简单:包没装,或者装了运行时但没装类型包。直接查node_modules和依赖树:

npm ls typescript
npm why @types/node
npm why @types/react

不少库现在自带类型声明,不需要@types;反过来有些老库只有@types一份。npm why能看到这个@types包是被谁依赖进来的,同一个包装出两个版本时类型解析只会挑其中一个,挑错的那个就开始报TS2307。

第二种是moduleResolution与实际的打包方式不匹配,这是升级后最常见的一种。取值有node(也叫node10)、node16、nodenext、bundler四种,bundler这个取值是TypeScript 5.0才加进来的。

怎么选看项目怎么跑:走Vite、webpack、Next.js这类打包器的配bundler;用tsc编译完直接让Node执行的配nodenext。

实测过一个Next.js项目被改成nodenext之后,相对导入必须写.js扩展名,不写就满屏TS2307,改回bundler立刻干净。原因是nodenext严格照着Node的ESM规则来,而打包器根本不看扩展名。

第三种是路径别名。tsconfig里配了paths,打包器那边没配同样的别名,于是运行时能跑、类型检查报错,也可能反过来。两边都得写:

// tsconfig.json
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }

// vite.config.ts
resolve: { alias: { '@': '/src' } }

第四种是自己写的.d.ts没被纳入编译。include只写了src,声明文件却放在项目根目录的types下,那就等于没写。用npx tsc --showConfig看一眼实际生效的include范围,比猜快。

两个能直接定位的命令

npx tsc --traceResolution会把每个模块的查找过程全打出来,输出很长,配合grep过滤模块名,就能看到它在哪些目录里找过、在哪一步放弃。另一个是npx tsc --explainFiles,用来查某个.d.ts究竟是被谁引进来的,查@types冲突时特别省事。

还有个容易忽略的:typeRoots或types字段一旦被显式配置,默认的@types扫描范围就被收窄了,只加载列出来的那几个包。之前接手过一个项目,tsconfig里写了types: ["node"],结果@types/jest完全没生效,测试文件里describe和it全线TS2307。

TS2339的几种典型场景

对象上真没这个属性是最直白的一种,TypeScript会把候选属性一并列出来,看那行Did you mean就够。

第二种是索引访问。拿变量当key去取对象属性,在没有索引签名的类型上直接报。要么给类型加索引签名,要么把key收窄成keyof:

const key = 'name' as keyof User;
const v = user[key];

第三种是往全局对象上挂属性。给window挂配置、或者从后端拿到结构不固定的数据,都属于这一类,写法是声明合并:

declare global {
  interface Window { __APP_CONFIG__: Record<string, string> }
}
export {};

末尾那行export {}不能省。没有它,这个文件会被当成脚本级的全局作用域,declare global反而不生效,这个坑踩过不止一次。

第四种是DOM事件回调。event.target的类型是EventTarget,不是具体的HTMLInputElement,直接取.value就报。写成(event.target as HTMLInputElement).value,或者改用event.currentTarget。

第五种是接口响应的取值位置不对。比如axios的响应数据在response.data里,直接取response.userInfo就会报TS2339 Property 'userInfo' does not exist。这不是配置问题,是泛型没传:

const { data } = await request<UserInfo>('/api/user');

skipLibCheck要不要开

第三方库的.d.ts里报TS2339,加skipLibCheck: true能立刻安静下来。但这么干之前先用npm why查一下,如果报错来自同一个@types包被装了两个版本,那是真问题,光靠skipLibCheck只是把噪音挡住,类型解析挑错的那个版本仍然存在。

另外strict系列开关一开,原先藏着的TS2339会成片冒出来。strictNullChecks下面,可能是undefined的值直接取属性就报,用可选链?.或者先判空。这一类最容易被误判成类型写错了。

几个容易混在一起的报错码

TS2305是模块找到了但没有这个导出成员,多半是版本升级后API改名;TS7016是隐式any缺类型声明,加@types或者用require导入绕过;TS2688是找不到某个库的类型定义文件,一般是lib配置里少了DOM或对应的ES版本。这三个和TS2307、TS2339经常一起出现,按码分开处理就行。

数据截至2026年10月,TypeScript 6.0.2、6.0.3,是6.0线上的版本(6.0.3发布于2026年4月16日),7.0起换成Go实现的原生编译器,命令名也变成了tsgo,老项目迁移时部分参数行为有变化。