ts-node(或nodemon) 提示找不到 TypeScript 类型声明?4个解决方案完整代码奉上

3,023 阅读4分钟
// 软件版本
操作系统:Mac OS Monterey Version 12.2.1
TypeScript: 4.6.2
ts-node: 10.7.0
nodemon: 2.0.15
node: v16.13.0

使用 ts-node 时,可能会出现找不到 TypeScript 类型定义的问题,官方也给出了明确的回复如下:

ts-node does not use files, include or exclude, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. Gulpfile.ts, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. ts-node index.ts) and TypeScript resolves dependencies based on imports and references.

简单翻译(真简(随)单(遍)翻译,手动狗头🐶)一下就是:ts-node 默认不使用 tsconfig.json 中的 files, include 或者 exclude 配置,而是从入口文件开始,根据文件依赖路径去查找编译文件。那这和 TypeScript 类型声明找不到有什么关系呢?

关系就在于:一般情况下,开发者自定义的的 *.d.ts 不会在常规的 *.ts 文件中显式的引入,而是依赖 TypeScript 自动识别,也就是说,TypeScript 自动读取了 *.d.ts 中的类型声明,这样普通的 *.ts 文件无需任何配置即可使用 *.d.ts 中声明的类型。那么问题就来了,ts-node 默认不使 用 tsconfig.json 的 files, include or exclude 这几个配置,也就是说,存在 *.d.ts 没有被自动识别并报错类型定义找不到的问题。

既然知道了为什么,解决方案也就呼之欲出,下面几个解决方案以供参考。

这里先给出后续示例的一些公共配置

1. 项目目录

├── package.json
├── src
│   └── index.ts
├── tsconfig.json
├── typings
└── yarn.lock

2. package.json

{
  "name": "ts-test",
  "version": "1.0.0",
  "main": "src/index.ts",
  "license": "MIT",
  "scripts": {
    "start": "nodemon ./src/index.ts"
  },
  "dependencies": {
    "axios": "^0.26.1",
    "nodemon": "^2.0.15",
    "ts-node": "^10.7.0"
  },
  "devDependencies": {
    "typescript": "^4.6.2"
  }
}

3. tsconfig.json

{
  "compilerOptions": {
    "target": "es5",                          
    "module": "commonjs",                     
    "lib": ["ES2015", "dom"],                 
    "strict": true,                                        
    "esModuleInterop": true,                  
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*", "typings/**/*"]
}

方案1:使用 typeRoots 配置项

typeRoots 是 tsconfig.json 的一个配置项,用来指定声明文件的存储位置(官方链接),默认情况下,不需要手动设置,TypeScript 会自动读取相对路径为 ./node_modules/@types/, ../node_modules/@types/, ../../node_modules/@types/ 等等的配置。

下面给出使用 typeRoots 的完整代码。

1. 项目目录

├── src
│   └── index.ts
├── tsconfig.json
├── typings
│   └── TestModule // 模块名是 TestModule
│       └── index.d.ts // 这里 declare module TestModule 的 TestModule 必须和模块名(目录名)一致

2. tsconfig.json

{
  "compilerOptions": {                        
    "typeRoots": ["./typings"], // 增加这一条配置即可,其他同默认配置           
  },
}

3. typings/TestModule/index.d.ts

declare module TestModule { 
  interface TestModuleAnimal { 
    legs: number,
  }
}

4. src/index.ts

const bird: TestModule.TestModuleAnimal = { 
  legs: 2,
}
console.log(bird)

关于使用 typeRoots 有几个点需要注意一下

  • 类型声明文件必须以 index.d.ts 命名,且存储在对应模块目录下,如上示例必须放在 typings/TestModule 目录下;
  • 声明的模块名必须和目录名一致,如上示例 declare module TestModule 这里的 TestModule 必须和对应的目录名一致;
  • 模块必须放在 typeRoots 声明的文件夹下,如上示例 TestModule 目录必须在 typings 目录下;

以下是使用范式:

1. 项目结构

<project_root>/
-- tsconfig.json
-- typings/
  -- <module_name>/
    -- index.d.ts

2. 声明文件格式

declare module '<module_name>' {
    // module definitions go here
}

方案2:使用 ts-node: { files } 配置

还有一种方式就是让 ts-node 识别 tsconfig.json 的 files, include or exclude 配置,这样 *.d.ts 的文件只要存放在 files 或者 include 指定的文件夹中即可,ts-node 会自动读取并编译。

1. 项目目录

src
│   └── index.ts
├── tsconfig.json
typings
│   └── API.d.ts
└── yarn.lock

2. tsconfig.json

{
  "ts-node": {
    "files": true // 增加这一条配置即可,其他同默认配置
  },
}

3. typings/API.d.ts

declare namespace API { 
  interface Weirongjing {
    a: number,
  }
}

4. src/index.ts

const test: API.Weirongjing = { 
  a: 1,
}
console.log(test)

细心的同学会发现,和方案1不同的是,这里的类型声明文件 API.d.ts 是直接放在 typings 目录下的,且命名空间可任意命名。

方案3:使用  /// <Reference path="类型声明文件路径" />

TypeScript 提供了 /// <Reference path="类型声明文件路径" />API 来显式的导入类型声明文件,完整代码如下:

1. 项目目录

src
│   └── index.ts
├── tsconfig.json
typings
│   └── API.d.ts
└── yarn.lock

 2. typings/API.d.ts

declare namespace API { 
  interface Weirongjing {
    a: number,
  }
}

3. src/index.d.ts

/// <reference path="../typings/API.d.ts" /> // 增加这一行即可
const test: API.Weirongjing = { 
  a: 1,
}
console.log(test)

使用 reference 显式引入的话,必须使用 declare namespace 来声明类型,无需其他配置。

方案4:使用 ESModule 的方式导入

使用 reference 方案的前提是使用 TypeScript 提供的命名空间(namespace),本质也是解决 JavaScript 的模块化问题,而 ES6 已经默认支持了模块化,那么则可以使用模块导入的方式,代码如下:

1. 项目目录

src
│   └── index.ts
├── tsconfig.json
typings
│   └── API.d.ts
└── yarn.lock

 2. typings/API.d.ts

export interface API {
  a: number,
}

3. src/index.d.ts

import { API } from '../typings/API'

const test: API = { 
  a: 2,
}
console.log(test)

小结

小结一下,可以使用4种方式来解决 ts-node 不识别 TypeScript 声明文件的问题

  1. 使用 tsconfig.json 的 typeRoots 配置项;
  2. 使用 tsconfig.json 的 ts-node: { files: true } 配置项;
  3. 使用 TypeScript 提供的 /// <reference path="类型声明文件路径" />
  4. 使用 ESModule 导入;

最后补充一点,如果使用 tsc 来编译项目,只需要把对应的声明文件放在 tsconfig.json 的 include 或者 files 配置项声明的目录中即可。