d.ts 是"形状说明书": 消费方只看它不看实现 — declare 描述世界, merge 扩展它
.d.ts 是一份只写形状不给实现的说明书: 你的库发布后, 消费方的编译器不看你那 8000 行实现, 只看这份说明书来决定类型对不对。declare 家族则是"向编译器宣告别处存在的东西" — 这个全局变量是构建器注入的、这个老库没有类型但长这样、这个 *.css 导入是合法的。
而声明合并是这套体系的放大器: interface 天生可叠加, 于是你能"钻进"第三方库的类型里加字段 — 给 Express 的 Request 挂上 user, 给 Window 挂上全局常量。三个词概括本页: 说明书(d.ts)、宣告(declare)、合并(merge)。
// dist/index.d.ts export declare function format(n: number): string; // 没有 { } 实现体 — 只有形状
npm i lodash
npm i -D @types/lodash # 社区说明书, 不装就 any 化declare module 'legacy-widget' { export function mount(el: HTMLElement, opts?: object): void; }
export {}; // 让文件变模块 declare global { interface Window { __ENV__: 'dev' | 'prod'; } }
declare const __APP_VERSION__: string; console.log(__APP_VERSION__); // 编译认账, 实现由构建器给
declare module '*.module.css' { const classes: Record<string, string>; export default classes; }
import 'vue'; declare module 'vue' { interface ComponentCustomProperties { $api: ApiClient; } }
interface Box { a: 1 } interface Box { b: 2 } const b: Box = { a: 1, b: 2 }; // 合并生效
{ "esModuleInterop": true }
import fs from 'fs'; // ✓ 默认导入 CJS 整包// scripts/global.d.ts (无 import/export 的脚本文件) /// <reference types="vite/client" />
// env.d.ts 忘写 export {} → 里面的 interface 变全局npm pack
# ../check-project: npm i ../mylib-1.0.0.tgz && tsc --noEmit核心依赖的私有 SDK 是裸 JS, import 后全 any。按"我们用到什么补什么"写最小声明:
// types/legacy-sdk.d.ts declare module '@corp/legacy-sdk' { export interface SdkOpts { endpoint: string; timeout?: number } export class Client { constructor(opts: SdkOpts); query<T>(sql: string): Promise<T[]>; close(): void; } } // 原则: 只声明用到的面, 库升级再补 — 别猜全貌
auth 中间件塞的 req.user 在路由里全是 any。augmentation 一次, 全仓受益:
// types/express.d.ts import type { User } from '../src/auth'; declare global { namespace Express { interface Request { user?: User } } } // 路由里: req.user?.id 直接有完整类型
用户报"没有类型定义"。检查发布三件套是否齐全:
{ "types": "./dist/index.d.ts",
"exports": { ".": {
"types": "./dist/index.d.ts", // 条件里也要有!
"import": "./dist/esm/index.js" } },
"files": ["dist"] }
# 本地验证: npm pack → 干净目录安装 → tsc --noEmit
import.meta.env.VITE_API 全是 any。给 vite/client 的类型扩展补上字段:
// src/vite-env.d.ts /// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_BASE: string; readonly VITE_SENTRY_DSN?: string; } interface ImportMeta { readonly env: ImportMetaEnv }
内部包不想为了类型走构建产物。tsconfig paths 指向源码入口:
{ "paths": {
"@repo/ui": ["../ui/src/index.ts"] } }
// 类型随源码实时更新, 不存在 d.ts 漂移
// 发布给外部时才走构建 + d.ts
@types 某字段标错, 全仓跟着错。轻量方案: 局部 augmentation 纠偏, 或 patch 包:
// 方案 A: 扩展修正(加正确形状, 绕开错的) declare module '@types-heavy/lib' { interface Opts { retries: number; } // 合并覆盖语义 } // 方案 B: pnpm patch 官方包的 d.ts, CI 锁补丁
直接 import 配置 JSON, resolveJsonModule 给出字面量推断:
{ "resolveJsonModule": true, "moduleResolution": "bundler" }
import pkg from '../package.json';
pkg.version; // 类型是字面量, 不是 any
import styles from './x.module.css' 报红。资源声明文件一次到位:
// types/assets.d.ts declare module '*.module.css' { const c: Record<string, string>; export default c; } declare module '*.svg' { const url: string; export default url; }
给既有枚举补 helper 命名空间, 声明合并让调用面更顺:
enum OrderStatus { Paid, Shipped } namespace OrderStatus { export function isFinal(s: OrderStatus) { return s === OrderStatus.Shipped; } } OrderStatus.isFinal(OrderStatus.Paid); // 同名合并出 helper
发布后发现 d.ts 引用了没发布的内部类型。流水线加"安装即编译"关卡:
# ci/verify-types.sh npm pack mkdir -p /tmp/consumer && cd /tmp/consumer npm init -y && npm i ../mylib-*.tgz typescript echo 'import { api } from "mylib"; api.users();' > t.ts npx tsc --noEmit t.ts # 声明不完整这里当场爆
{ "emitDeclarationOnly": true } # 强制走生成export {}。 // env.d.ts 末尾: export {}; 让声明局部化// 对: import 'express'; declare module 'express-serve-static-core' {...}
// IDE: cmd+click req.user → 看它声明在哪个模块// 对: window.__APP__?.user 而不是 window.usernpm i -D @types/node@20 # 跟 node 大版本走npm rm @types/vue # vue 3 自带类型// 手写 d.ts: import { U } from './user' ← user.d.ts 必须真存在// 模块里: import type {} from 'vite/client'; # 或 reference 也行但更推荐 paths
new DefinePlugin({ __APP_VERSION__: JSON.stringify(v) })namespace Utils {} # 别再当模块用
{ "esModuleInterop": true } # 现代 tsconfig 默认带// exports "." 条件里没有 types → bundler 模式下丢失// 生成器(如 api-extractor) 拍平引用// declare enum 两处声明 → 成员集不确定// (window as any).foo ✗ → declare global interface Window { foo?: Foo }pnpm patch @heavy/types # 补丁进版本控制// 模块文件内: declare global { interface Window {...} }
// vite 官方 client.d.ts 已含常用资源, 优先 reference 它npx tsc --noEmit consumer.ts # 干净目录跑