1. 技术难点:为什么 TS 不认识第三方 JS 库
你在 TypeScript 项目里 import 一个用 JS 写的第三方库(如某个老 npm 包),TS 会报错:
arduino
Could not find a declaration file for module 'some-lib'.
原因:TS 需要".d.ts 声明文件"来描述模块的"形状" (导出哪些函数、什么参数类型)。如果这个库没自带类型声明,TS 就"不认识"它,于是:
- import 报错/变成
any - 用错参数、属性拼错,都没有任何类型提示和检查
核心难点 :如何正确地写 .d.ts,让 TS 认识:
- 无类型的 JS 库
- 引入的全局变量(CDN 的
$、_等) - 非 JS 资源(
.css、.png、.vue、.json) - 全局方法/环境(
process.env、import.meta)
2. 完整解法:声明文件的分层写法
2.1 三种声明文件的位置
TS 会按以下优先级找类型:
| 位置 | 说明 |
|---|---|
@types/xxx(@types 命名空间) |
社区维护的公认库类型(@types/jquery) |
库自带 types/typings 字段指向的文件 |
库作者写的 |
项目内 declare module 'xxx' |
你自己补的(shim) |
2.2 给 JS 库"声明模块"(最常见)
没有类型时,先在项目里建一个 xxx.d.ts 手写声明:
ts
// 方法一:极简(但不推荐,会让类型是 any)
declare module 'some-lib';
// 方法二:精确声明导出(推荐)
declare module 'some-lib' {
export function formatDate(date: Date, format?: string): string;
export const version: string;
export interface Options { locale?: string; timezone?: string; }
export function parse(input: string, options?: Options): Date;
}
// 方法三:默认导出
declare module 'my-lib' {
declare function main(): void;
export default main;
}
2.3 声明全局变量(CDN / 挂 window)
如果库通过 <script> 引入,挂在全局上(不是 import),声明全局:
ts
// global.d.ts
declare var $: any; // 太宽, 不推荐
declare function $... : any; // 简略
// 规范做法:声明 jQuery 类型
interface JQuery { ... }
declare const $: {
(selector: string): JQuery;
ajax(options: object): void;
};
给全局对象挂自定义属性 (如 window.analytics):
ts
// declaration.d.ts
declare global {
interface Window {
analytics?: { track: (event: string) => void };
}
}
export {};
2.4 声明非 JS 资源模块(CSS/图片/Vue)
webpack/Vite 引入 .css、.png、.vue 需要 shim,否则 import 报错:
ts
// shims.d.ts
declare module '*.css'; // CSS 模块
declare module '*.png'; // 图片
declare module '*.svg';
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
2.5 声明全局环境(process.env / import.meta)
Node 环境或需要 process.env.XXX 有类型:
ts
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
API_BASE?: string; // 声明你能访问的环境变量,别的访问报错
SECRET_KEY?: string;
}
}
3. 实战:从一个无类型 JS 库到完整声明
假设老库 legacy-util.js 提供了一个 debounce:
js
// legacy-util.js(无类型)
exports.debounce = function (fn, wait) { /* ... */ };
exports.stringify = function (obj) { return JSON.stringify(obj); };
给它写 legacy-util.d.ts:
ts
// legacy-util.d.ts
declare module 'legacy-util' {
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait?: number
): (...args: Parameters<T>) => void;
export function stringify(obj: unknown, indent?: number): string;
}
之后 import { debounce } from 'legacy-util' 就有完整的类型提示了。
用 declare module 时注意:
- 必须在模块文件 (有 import/export)里写,或用
export {}隔离,否则会污染全局。 declare module 'x'匹配的是"模块解析",declare global匹配全局。
4. 要点总结
- 声明文件 .d.ts:描述 JS 模块形状,让 TS 认识无类型的库/全局/资源。
- 模块声明 :
declare module 'xxx' { export ... },精确写导出函数/类型。 - 全局声明 :CDN/挂 window 用
declare global+declare var/const。 - 资源 shim :
.css/.png/.vue需要declare module '*.xxx'。 - 类型共享:能发布自己写的 .d.ts 给团队,良好类型也是一等公民。
- 优先用社区
@types/xxx,缺失才自己写 shim;生产库建议通过types字段随包发布类型。 - 别用
declare module 'x';(无花括号)当甩锅------它让类型变any,失去检查价值。
一句话:.d.ts 是 TS 和 JS 世界的"翻译官"------给无类型的库/全局/资源补上形状描述,TS 才能帮你检查、提示、约束。会写声明文件,就不怕"TS 项目遇到老 JS 库"了。