声明文件(.d.ts)只包含类型,别无其他:为存在于别处的 JavaScript 提供签名、接口和类的形状。declare 关键字在普通文件中做的是同一件事。它告诉编译器“这个东西在运行时存在,相信我”,并且不生成任何代码。
编译器接受了 __APP_VERSION__.length,因为声明说它是字符串。但运行时并没有这个变量,所以读取 .length 会抛出 ReferenceError: __APP_VERSION__ is not defined,这是编译器完全没有预料到的运行时异常。这就是每个声明背后的约定:类型有多可靠,完全取决于它背后的 JavaScript。
.d.ts 文件里有什么
用 declaration: true 编译最能说明问题。对于下面的源代码:
// price.ts
export interface LineItem {
name: string;
price: number;
qty: number;
}
const TAX = 0.2;
export function total(items: LineItem[]) {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
return Math.round(sum * (1 + TAX) * 100) / 100;
}
export class Cart {
private items: LineItem[] = [];
add(item: LineItem) {
this.items.push(item);
return this;
}
}
tsc 会生成 price.js 和下面这个 price.d.ts:
export interface LineItem {
name: string;
price: number;
qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
private items;
add(item: LineItem): this;
}
函数体没有了,推断出的返回类型被显式写了出来(number、this),私有字段保留了名字却丢掉了类型,未导出的 TAX 则完全不见了。库会发布 .js 给 Node 使用,发布 .d.ts 给你的编辑器和编译器使用。emitDeclarationOnly: true 只生成 .d.ts 文件,适用于由打包工具构建 JavaScript 的项目。
你每天都在用的 ES2022 内置对象也来自声明文件:lib.es2022.d.ts 等文件随 TypeScript 一起发布,并由 target 和 lib 选择加载哪些。
declare 的几种形式
每条 declare 语句描述的都是已经存在的东西。在没有 import 或 export 的 .d.ts 文件(全局声明文件)中,下面每一项都会对整个项目可见:
// globals.d.ts
declare const API_URL: string; // a global constant
declare let debugMode: boolean; // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget { // a class from a script tag
constructor(el: string);
render(): void;
}
declare namespace Analytics { // a global object
function page(name: string): void;
}
declare module "legacy-charts" { // a module you import
export function draw(data: number[]): void;
}
在 .d.ts 文件中,interface 和 type 不需要 declare;其他任何没有 declare 或 export 的顶层声明都会报 TS1046 错误。一旦文件含有 import 或 export,其中的声明就只在该文件内有效,要向全局作用域添加内容,就得放进 declare global 块(见下文)。编译器会依据声明的签名来检查调用:
编译器报告 index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'.。可以改为传入 { items: "3" };如果真实的函数接受数字,就修改声明。
@types 包
许多 npm 包自带 .d.ts 文件,通过 package.json 中的 types 字段(或 exports 中的 types 条件)引用。对于只有 JavaScript 的包,由社区维护的 DefinitelyTyped 项目会在 @types 作用域下发布类型:
npm i lodash
npm i -D @types/lodash
当你 import 一个包时,TypeScript 会自动查找 node_modules/@types/{name}。描述全局内容而不是某个导入的类型,比如 @types/node(process、Buffer)或测试框架的 describe 和 it,必须在 tsconfig.json 中列出:
{
"compilerOptions": {
"types": ["node"]
}
}
没有这一项,即使包已经安装,TypeScript 7 也不会加载它们:
error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
tsc --init 会在新配置中写入 "types": [],并附上一条注释,建议 Node 项目改为 ["node"]。
为没有类型的模块编写类型
导入一个既没有自带类型、在 @types 中也找不到类型的 JavaScript 包,会报 TS7016 错误:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
修复方法是在项目中任意位置添加一个没有顶层 import 或 export 的 .d.ts 文件(常见做法是放在 types/ 文件夹中,只要它在 include 的范围内即可),并描述你用到的部分:
// types/fakelib.d.ts
declare module "fakelib" {
export function hi(name: string): string;
export const version: string;
}
// Non-code files a bundler lets you import
declare module "*.svg" {
const url: string;
export default url;
}
最简短的写法是只写一行 declare module "fakelib";,它会让这个包的所有导入都变成 any。错误消失了,所有检查也随之消失,所以只把它当作临时措施。
declare global
向全局作用域添加内容的代码,比如 polyfill、内置对象上的新方法,或者由 script 标签设置的全局变量,都需要对应的类型。declare global 用来添加这些类型。它必须放在模块中(含有 import 或 export 的文件),这就是顶部有 export {} 的原因:
interface Array<T> 会与内置的 Array 接口合并,而不是替换它;能给 globalThis 添加属性的是 var(而不是 let 或 const)。在共享代码中扩展内置原型是有风险的;项目也正是用同样的 declare global 技巧,通过 @types/node 的 NodeJS.ProcessEnv 接口,把自己的变量添加到 process.env 中。
模块扩充
要给你导入的包补充类型,就重新声明它的模块名并重新打开接口。这个文件本身必须是模块(import 让它成为模块;export {} 也可以)。如果没有 import 或 export,同样的代码块会声明一个全新的 config-lib 模块,反而把包真正的类型遮盖掉:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
这样一来,config-lib 中 load().beta 的类型在所有地方都是 boolean。接口会合并,类型别名则不会,所以库必须导出 interface 才能这样做。插件就是用这种方式给框架的请求对象或配置对象添加字段的。
skipLibCheck
skipLibCheck: true 让编译器不再对 .d.ts 文件做类型检查,包括 node_modules 中的文件。你自己的代码仍然会依据它们进行检查。这能节省时间,还能避免两个包的声明相互冲突导致的错误,所以 tsc --init 会默认开启它。代价是你自己 .d.ts 文件里的错误也不会被报告。
常见问题
TypeScript 中的 .d.ts 文件是什么?
声明文件:它只包含类型(函数签名、接口、类的形状),没有任何实现。它描述存在于别处的 JavaScript,比如编译好的库或浏览器内置的 API,让 TypeScript 能检查使用这些代码的地方。tsc 从不为它生成 JavaScript。
TypeScript 中的 declare 关键字有什么作用?
它告诉编译器某个值在运行时存在,但并不创建这个值。declare const VERSION: string; 让你可以把 VERSION 当作字符串使用,而这一行会从输出中消失。如果实际上没有任何地方定义 VERSION,程序会在运行时因 ReferenceError 而失败。
如何解决 “Could not find a declaration file for module” 错误?
TS7016 错误表示这个包没有类型。如果有现成的类型就安装它(npm i -D @types/package-name),否则添加一个 .d.ts 文件,用 declare module "package-name" { ... } 描述你用到的部分。只写一行 declare module "package-name"; 会把整个模块的类型当作 any,从而让错误消失。
如何从 TypeScript 生成 .d.ts 文件?
在 tsconfig.json 中设置 "declaration": true(或传入 --declaration)。这样每个 .ts 文件都会在它的 .js 旁边生成一个 .d.ts。如果由其他工具构建 JavaScript,就加上 emitDeclarationOnly;加上 declarationMap 则可以让编辑器从类型跳转到你的源代码。
declare global 和 declare module 有什么区别?
declare global { ... } 向全局作用域添加内容,比如给 Array 新增一个属性,或者添加一个全局变量。declare module "name" { ... } 描述一个你用这个名字导入的模块。declare global 必须放在模块中(含有 import 或 export 的文件);declare module 在没有导入导出的文件中声明一个新模块,在模块内部则是扩充已有的模块。