Menu

TypeScript declare:.d.ts 声明文件详解

.d.ts 文件描述 JavaScript 代码的类型,却不包含任何代码本身,而 declare 关键字在 .ts 文件中起同样的作用。本文讲解声明文件如何生成、@types 包的作用、如何为没有类型的模块编写类型,以及 declare global 和模块扩充如何扩展已有类型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

声明文件(.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 在没有导入导出的文件中声明一个新模块,在模块内部则是扩充已有的模块。

Coddy 编程语言插图

和 Coddy 一起学编程

开始