声明文件与 .d.ts
声明文件的角色
声明文件(.d.ts)是 TypeScript 类型系统与 JavaScript 生态之间的桥梁。它只包含类型信息,不包含运行时代码,告诉编译器某个 JavaScript 模块的形状(shape)。理解声明文件的编写和加载机制,是在大型项目中维护类型安全的关键能力。
面试定位:声明文件问题通常出现在"如何给没有类型的第三方库添加类型"或"如何扩展已有库的类型定义"这类实际工程场景中。
声明文件基础
声明语法
.d.ts 文件中使用 declare 关键字描述外部存在的值:
// globals.d.ts — 描述全局变量
declare const __DEV__: boolean;
declare const __APP_VERSION__: string;
// 描述全局函数
declare function ga(command: string, ...args: any[]): void;
// 描述全局对象
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
DATABASE_URL: string;
}
}
模块声明
// types/untyped-lib.d.ts
declare module "untyped-lib" {
export function process(input: string): string;
export interface Config {
timeout: number;
retries: number;
}
export default function init(config: Config): void;
}
通配符模块声明
处理非 JavaScript 导入:
// types/assets.d.ts
declare module "*.svg" {
const content: React.FC<React.SVGProps<SVGSVGElement>>;
export default content;
}
declare module "*.css" {
const classes: Record<string, string>;
export default classes;
}
declare module "*.png" {
const src: string;
export default src;
}
类型声明的加载机制
三种来源
TypeScript 从三个位置加载类型声明:
- 内置声明(lib):由
tsconfig.json的lib字段控制,如"DOM","ES2022"等 - @types 包:
node_modules/@types/下的声明包,由types或typeRoots配置控制 - 项目内声明:项目中的
.d.ts文件,由include配置控制
@types 与 DefinitelyTyped
DefinitelyTyped 是社区维护的类型声明仓库,通过 @types/xxx 包名发布:
npm install --save-dev @types/lodash
npm install --save-dev @types/node
自动加载规则:
- 默认情况下,
node_modules/@types/下的所有包都会被自动加载 - 通过
tsconfig.json的types字段可以限制只加载指定的@types包 typeRoots字段可以改变类型声明的搜索路径
{
"compilerOptions": {
"types": ["node", "jest"],
"typeRoots": ["./types", "./node_modules/@types"]
}
}
库自带类型 vs @types
现代库的趋势是自带类型声明(在 package.json 中指定 "types" 字段):
{
"name": "my-lib",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
判断库是否有类型的优先级:
- 库自带
types字段 → 直接使用 - 对应
@types/xxx包存在 → 安装使用 - 两者都没有 → 需要手动编写声明文件
模块增强(Module Augmentation)
扩展第三方库的类型
// types/express-augmentation.d.ts
import "express";
declare module "express" {
interface Request {
user?: {
id: string;
role: "admin" | "user";
};
requestId: string;
}
}
扩展 Window 对象
// types/global.d.ts
declare global {
interface Window {
__INITIAL_STATE__: Record<string, unknown>;
gtag: (...args: any[]) => void;
}
}
export {}; // 确保文件被视为模块
关键:包含 import 或 export 的文件是模块文件,全局声明必须包裹在 declare global {} 中。没有 import/export 的 .d.ts 文件是全局脚本文件,声明直接生效。
扩展已有接口
// 扩展 React 的 CSSProperties
import "react";
declare module "react" {
interface CSSProperties {
"--primary-color"?: string;
"--spacing"?: string;
[key: `--${string}`]: string | undefined;
}
}
全局类型扩展
环境变量类型
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
DATABASE_URL: string;
API_KEY: string;
PORT?: string;
}
}
全局工具类型
// types/utils.d.ts
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
为无类型库编写声明
完整声明
// types/legacy-analytics.d.ts
declare module "legacy-analytics" {
interface AnalyticsConfig {
trackingId: string;
debug?: boolean;
sampleRate?: number;
}
interface EventPayload {
category: string;
action: string;
label?: string;
value?: number;
}
export class Analytics {
constructor(config: AnalyticsConfig);
track(event: EventPayload): void;
page(path: string): void;
identify(userId: string, traits?: Record<string, unknown>): void;
}
export function createAnalytics(config: AnalyticsConfig): Analytics;
export default createAnalytics;
}
渐进式声明策略
对于大型无类型库,可以渐进添加类型:
// 阶段一:最小可用声明
declare module "complex-lib";
// 所有导入类型为 any
// 阶段二:主要 API 声明
declare module "complex-lib" {
export function mainApi(input: string): Promise<Result>;
// 其余仍为 any
}
// 阶段三:完整声明
// 逐步补充所有导出的类型
声明文件的项目组织
推荐结构
project/
├── src/
├── types/
│ ├── env.d.ts # 环境变量
│ ├── global.d.ts # 全局类型扩展
│ ├── assets.d.ts # 资源文件模块声明
│ └── legacy-lib.d.ts # 无类型库声明
├── tsconfig.json
tsconfig 配置
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist/types",
"declarationMap": true
},
"include": ["src/**/*", "types/**/*"]
}
declaration: 编译时自动生成.d.ts文件declarationMap: 生成声明文件的 source map,支持"跳转到源码"declarationDir: 声明文件输出目录
面试高频问题
Q: 如何给一个没有类型定义的第三方库添加类型?
回答要点:三步走策略。首先检查 @types/xxx 包是否存在;若不存在,在项目的 types/ 目录下创建 xxx.d.ts,使用 declare module "xxx" 声明模块类型,并确保 tsconfig.json 的 include 覆盖该目录。声明可以从最简单的 declare module "xxx" 开始(所有导入为 any),逐步完善具体 API 的类型签名。
Q: declare module 和 declare namespace 有什么区别?
回答要点:declare module "xxx" 声明的是 ES module 或 CommonJS 模块的类型,对应 import xxx from "xxx" 的导入方式。declare namespace 声明的是全局命名空间,用于描述通过 <script> 标签加载的全局库,或者 Node.js 的全局对象(如 NodeJS 命名空间)。
Q: 为什么全局声明有时需要 export {}?
回答要点:TypeScript 将没有 import/export 语句的文件视为全局脚本,其中的声明自动在全局作用域生效。一旦文件包含 import 或 export,就变成了模块文件,全局声明必须包裹在 declare global {} 中。添加空的 export {} 是将文件显式标记为模块的常见技巧。