TypeScript 声明文件与互操作
TypeScript 最大的工程难题不是”怎么写类型”,而是**“如何让已有 JS 生态在类型系统下工作”**。声明文件(.d.ts)和互操作机制要解决的就是这个问题。
对应 Python 的 stub 文件(.pyi)——为没有类型标注的代码补上类型信息。但 TS 的声明文件远比 .pyi 复杂,因为它要覆盖 JS 的各种”动态魔术”:原型链扩展、this 类型、函数重载、全局变量污染——这些 JS 独有的模式在 Python 的类型世界中没有对应物。
.d.ts 的核心角色
是什么
.d.ts 文件是 TypeScript 的”类型声明文件”——只包含类型信息,不包含运行时逻辑。编译器用它们来类型检查,但从不生成对应的 JS 代码。
源码层面:
user.ts ← 既有类型又有逻辑
user.d.ts ← 只有类型声明,无逻辑
编译后:
user.ts → user.js (类型擦除,保留逻辑)
user.d.ts → (不输出 JS)
三种来源
| 来源 | 示例 | 场景 |
|---|---|---|
| 源码自带 | lodash 自带 index.d.ts | 库作者用 TS 写 |
| 社区补充 | npm install @types/lodash | JS 库没有类型 → DefinitelyTyped |
| 项目自写 | src/types/global.d.ts | 针对项目特定的 JS 代码/全局变量 |
DefinitelyTyped:全球最大的类型协作仓库
什么是 DefinitelyTyped
@types/* 包来自 DefinitelyTyped——一个由社区维护的独立于库作者的类型声明仓库。
# 安装 lodash 的类型声明(lodash 本身是 JS 库)
pnpm add lodash
pnpm add -D @types/lodash
# 现在 lodash 在 TS 中有完整的类型提示
import _ from "lodash";
_.chunk([1, 2, 3, 4], 2); // 类型:number[][]工作原理
你的代码:
import express from "express"
↓
TypeScript 编译器查找类型:
1. express 包内有 index.d.ts 吗? → 没有(express 是 JS 库)
2. @types/express 存在吗? → 有 → 使用它的类型
↓
express 在编译后有完整类型支持
常见问题与对策
| 问题 | 表象 | 对策 |
|---|---|---|
| 库没有 @types | Could not find declaration file | 自建 types/foo.d.ts + declare module |
| @types 版本不匹配 | 类型报错但运行时正常 | 锁定 @types 版本,或 fork 修复 |
| @types 质量差 | 大量 any,无实际约束 | 在 tsconfig 中启用 skipLibCheck |
| 库已内建类型但 @types 冲突 | 重复声明报错 | 移除 @types/*,用内建类型 |
// 自建声明文件 types/my-js-lib.d.ts
declare module "my-js-lib" {
export function doWork(input: string): Promise<number>;
export interface Options {
timeout: number;
retry: boolean;
}
}
// 现在可以在代码中正常引用
import { doWork } from "my-js-lib";常用声明技巧
declare module:为无类型模块补类型
// 为 .css/.scss 模块声明类型(Vite 常见需求)
declare module "*.css" {
const content: Record<string, string>;
export default content;
}
// 为全局变量声明类型
declare global {
interface Window {
__INITIAL_STATE__: Record<string, unknown>;
analytics: {
track(event: string, data?: unknown): void;
};
}
}
// 扩展已有模块的类型
declare module "express" {
interface Request {
user?: { id: string; role: string };
}
}declare namespace:为全局库声明类型
// 模拟 jQuery 的全局声明
declare namespace $ {
function ajax(url: string, options?: AjaxOptions): void;
interface AjaxOptions {
method?: "GET" | "POST";
data?: Record<string, unknown>;
}
}
// 使用
$.ajax("/api/users", { method: "GET" });ambient declarations:环境声明
// 声明一个运行时才存在的变量(如 CDN 引入的脚本)
declare const APP_VERSION: string;
declare function trackPageView(page: string): void;
// 编译器相信它们在运行时存在,不做静态验证
console.log(APP_VERSION);
trackPageView("/home");JS 与 TS 的互操作策略
从 JS 迁移到 TS:渐进式路线
TS 的一大卖点是渐进式采用——你不需要一次性把所有 .js 改成 .ts。
策略 1(推荐):先 `allowJs: true`,逐步改名 .js → .ts
策略 2:保留核心 JS 文件,新增文件用 TS
策略 3:JS 文件用 .d.ts 补类型,TS 文件直接类型化
// tsconfig.json — 渐进迁移的核心配置
{
"compilerOptions": {
"allowJs": true, // 允许 .js 文件参与编译
"checkJs": false, // 不检查 .js 文件的类型
"outDir": "dist"
},
"include": ["src/**/*"]
}TS 调用 JS:最平滑的方向
// legacy-utils.js — 旧的 JS 工具函数
function formatMoney(amount, currency) {
return `${currency}${amount.toFixed(2)}`;
}
module.exports = { formatMoney };// types/legacy-utils.d.ts — 为 JS 补充类型
declare module "legacy-utils" {
export function formatMoney(amount: number, currency: string): string;
}
// app.ts — TS 代码调用 JS,有类型保护
import { formatMoney } from "legacy-utils";
const price = formatMoney(19.99, "$"); // 类型安全
// formatMoney("19.99"); // 编译错误JS 调用 TS:需要构建产物
// new-feature.ts — 新写的 TS 代码
export function processData(input: string): { id: string; data: unknown } {
return { id: crypto.randomUUID(), data: JSON.parse(input) };
}// old-app.js — 旧 JS 代码引用编译后的 JS
const { processData } = require("./dist/new-feature.js");
processData('{"key": "value"}'); // 没有类型检查,但能正常运行与 Python 类型存根的对比
| 维度 | TS .d.ts | Python .pyi |
|---|---|---|
| 仓库 | DefinitelyTyped(社区统一) | typeshed(CPython 标准库) |
| 第三方库类型 | @types/foo 独立安装 | 库自带的 py.typed 标记或独立 stub 包 |
| 模块声明 | declare module "foo" 功能强大 | *.pyi 仅声明接口 |
| 全局声明 | declare global / declare namespace | 无直接等价能力 |
| 生态覆盖 | 极高(几乎所有流行 JS 库都有 @types) | 中等(许多库仍未提供类型) |
| 语法复杂度 | 高(需覆盖 prototype、this、overloads 等 JS 动态模式) | 低(Python 的动态行为相对有界) |
TS 的声明文件系统是为了将动态的、无类型的 JS 世界纳入静态类型系统而设计的,所以它必须处理大量 JS 独有的模式——原型链扩展、
this类型、函数重载、全局变量污染等。Python 的.pyi更多是”已有接口的静态表达”,不需要覆盖这些 JS 特有的难题。
常见声明陷阱
1. 函数重载声明
// 错误 — 声明签名与实际签名混在一起
function greet(name: string): string;
function greet(name: string, title: string): string; // 参数数量不同
// 正确 — 声明签名在上,实现签名在下(参数取并集)
function greet(name: string): string;
function greet(name: string, title: string): string;
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}2. export default vs export =
// CJS 模块 (module.exports = xxx) 的声明
declare module "legacy-lib" {
export = LegacyLib; // 对应 module.exports
}
declare function LegacyLib(config: any): void;
// ESM 模块 (export default) 的声明
declare module "modern-lib" {
export default function modernFunc(input: string): void;
}
// 混用会导致 import 方式不匹配3. 命名空间与模块的混淆
// 在模块文件(有 import/export)中用 declare namespace
declare namespace MyLib { }
// 在全局声明文件(无 import/export)中用 declare namespace
// 或在模块文件中用 declare module
declare module "my-lib" { }关联
- TS 类型系统 — 类型声明依赖的基础类型工具
- TS 编译与执行 — tsc 如何处理
.d.ts - meta/模块与可见性 — 跨语言模块设计
- Python 面向对象 — Python 的类型标注模型
- TypeScript 专题 MOC
- Python: 面向对象 — Python 的类型标注模型