在 TypeScript 中,声明文件
.d.ts就像是 JavaScript 库的"使用说明书"。它告诉 TypeScript 如何理解没有类型信息的代码。本篇文章将深入探讨如何编写高质量的声明文件,为任何 JavaScript 库添加类型安全。
声明文件基础
什么是声明文件
声明文件(.d.ts) 是只包含类型信息的 TypeScript 文件。它不包含具体的实现代码,只告诉 TypeScript 某个值的存在和它的类型。
我们可以看一个简单的例子,比如有一个JavaScript库,但没有类型信息:
window.myLibrary = {
version: "1.0.0",
greet(name) {
return `Hello, ${name}!`;
}
};
此时,我们需要为 window 创建一个类型声明:global.d.ts,用来告诉 TypeScript:window 上有一个 myLibrary 对象:
interface MyLibrary {
version: string;
greet(name: string): string;
}
declare global {
interface Window {
myLibrary: MyLibrary;
}
}
加上声明文件后,TypeScript 就能理解这个库了,我们就可以安全地使用:
console.log(window.myLibrary.version); // ✅ 类型安全
const greeting = window.myLibrary.greet("zhangsna"); // ✅ 知道返回string
// window.myLibrary.unknownMethod(); // ❌ 编译错误:方法不存在
为什么需要声明文件?
- 为纯 JavaScript 库添加类型支持。
- 描述已有 JavaScript 代码的类型。
- 共享类型定义(通过
@types包)。 - 提前声明尚未实现的接口。
在后面的文章中,会详细讲解 @types 包的工作原理与最佳实践。
全局声明 vs 模块声明
全局声明:会影响整个项目
全局声明 意味着在项目的任何地方都可以访问,不需要导入。通常用于:
- 全局变量(如window、document)。
- 库直接暴露在全局作用域。
- 自定义全局类型。
模块声明:需要导入才能使用
模块声明 需要通过 import 语句导入才能使用。现在大多数npm包使用的就是这种方式。
模块声明导出
// my-library.d.ts:描述一个外部模块
// 导出类型
export interface User {
id: number;
name: string;
email: string;
}
export type UserRole = "admin" | "user" | "guest";
// 导出函数
export function createUser(name: string, email: string): User;
export function getUserById(id: number): Promise<User>;
// 导出类
export class UserManager {
private users: User[];
addUser(user: User): void;
removeUser(id: number): boolean;
getAllUsers(): User[];
}
// 导出常量
export const DEFAULT_ROLE: UserRole;
// 默认导出
export default UserManager;
模块声明导入
// 使用模块声明
import UserManager, { User, createUser } from "my-library";
const manager = new UserManager();
const user: User = createUser("zhangsan", "zhangsan@example.com");
manager.addUser(user);
混合声明:同时支持全局和模块
有些库既支持全局使用,也支持模块导入。
// jquery.d.ts 的简化示例
// 模块导出
declare module "jquery" {
interface JQuery {
// jQuery方法
hide(): JQuery;
show(): JQuery;
css(property: string, value: string): JQuery;
// ... 更多方法
}
function $(selector: string): JQuery;
export = $;
}
// 全局声明(当通过script标签引入时)
declare global {
interface JQuery {
hide(): JQuery;
show(): JQuery;
css(property: string, value: string): JQuery;
}
const $: (selector: string) => JQuery;
}
// 使用方式1:模块导入
// import $ from "jquery";
// $("#myElement").hide();
// 使用方式2:全局使用(通过script标签引入后)
// $("#myElement").show();
declare关键字全解
declare的基本用法
declare 关键字告诉 TypeScript:"这个值在别处已经存在,我只是告诉你它的类型"。
// 声明变量
declare const VERSION: string;
declare let config: AppConfig; // 使用let表示可以重新赋值
// 声明函数
declare function calculate(x: number, y: number): number;
// 声明类
declare class Person {
name: string;
age: number;
constructor(name: string, age: number);
greet(): string;
}
// 声明枚举
declare enum Status {
Pending,
Active,
Inactive
}
// 声明命名空间
declare namespace MyApp {
interface Config {
apiUrl: string;
timeout: number;
}
function initialize(config: Config): void;
}
// 声明模块
declare module "my-module" {
export function doSomething(): void;
export const importantValue: number;
}
declare的进阶用法
声明全局增强:给已有类型添加新属性
declare global {
// 给String添加自定义方法
interface String {
toCamelCase(): string;
toSnakeCase(): string;
}
// 给Array添加方法
interface Array<T> {
findBy(predicate: (item: T) => boolean): T | undefined;
groupBy(key: keyof T): Record<string, T[]>;
}
}
声明合并:多次声明同一个接口,TypeScript会自动合并
interface User {
id: number;
name: string;
}
// 在另一个文件中可以扩展
interface User {
email: string; // 添加到User接口
}
// 最终User接口有:id, name, email
条件声明:根据不同环境声明不同的类型
declare const process: {
env: {
NODE_ENV: "development" | "production" | "test";
API_URL?: string;
DEBUG?: string;
};
};
类型守卫声明:声明一个函数是类型守卫
declare function isString(value: unknown): value is string;
declare function isUser(value: unknown): value is User;
declare的特殊语法
declare const vs declare let
// const:声明常量,不能重新赋值
declare const API_KEY: string;
// API_KEY = "new"; // ❌ 错误
// let:声明变量,可以重新赋值
declare let currentUser: User | null;
currentUser = { id: 1, name: "Alice" }; // ✅ 可以
declare function 的函数重载
declare function createElement(tag: "div"): HTMLDivElement;
declare function createElement(tag: "span"): HTMLSpanElement;
declare function createElement(tag: string): HTMLElement;
declare class 的抽象类
declare abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("Moving...");
}
}
declare namespace 的嵌套
declare namespace Geometry {
export interface Point {
x: number;
y: number;
}
export namespace Shapes {
export interface Circle {
center: Point;
radius: number;
}
export interface Rectangle {
topLeft: Point;
width: number;
height: number;
}
}
}
// 使用:Geometry.Shapes.Circle
为无类型库添加类型支持
分析JavaScript库的API:了解库的使用方式
// 全局变量形式
window.calculator = {
PI: 3.14159,
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; },
multiply: function(a, b) { return a * b; },
divide: function(a, b) { return a / b; },
// 高级功能
calculate: function(operation, a, b) {
switch(operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
case 'divide': return a / b;
default: throw new Error('Unknown operation');
}
}
};
创建类型声明文件:根据API编写声明文件
interface Calculator {
// 常量
readonly PI: number;
// 基本运算方法
add(a: number, b: number): number;
subtract(a: number, b: number): number;
multiply(a: number, b: number): number;
divide(a: number, b: number): number;
// 高级方法
calculate(
operation: "add" | "subtract" | "multiply" | "divide",
a: number,
b: number
): number;
// 可能还有错误处理
lastError?: string;
clearError(): void;
}
// 全局声明
declare global {
// 挂载在window上
interface Window {
calculator: Calculator;
}
// 也可以直接暴露为全局变量(如果库也这样做了)
const calculator: Calculator;
}
// 如果库也支持模块导入,添加模块声明
declare module "simple-calculator" {
const calculator: Calculator;
export = calculator;
}
// 现在TypeScript就能理解calculator库了
发布和维护类型声明
发布到 @types 仓库
// package.json配置示例
{
"name": "@types/my-library",
"version": "1.0.0",
"description": "TypeScript definitions for my-library",
"license": "MIT",
"contributors": [
{
"name": "Your Name",
"githubUsername": "yourusername"
}
],
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git"
},
"scripts": {},
"dependencies": {
// 如果有依赖的类型包
"@types/node": "*"
},
"typesPublisherContentHash": "",
"typeScriptVersion": "4.5"
}
与源库捆绑发布
// 在库的package.json中
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts", // 指向类型声明文件
"files": [
"dist",
"*.d.ts" // 包含声明文件
],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
常见问题与解决方案
如何处理过于动态的API?
// 使用索引签名或any类型
interface VeryDynamicAPI {
[key: string]: any; // 允许任意字符串键,任意值
[key: number]: string; // 允许数字键,必须是string值
// 或者更精确的
[key: `get${string}`]: () => any; // 模板字面量类型
[key: `set${string}`]: (value: any) => void;
}
// 或者使用类型断言
declare const dynamicLib: any; // 整个库都是any
const result = (dynamicLib as { doSomething: () => number }).doSomething();
如何处理依赖的类型?
// 在声明文件中声明依赖
declare module "my-library" {
import { Request, Response } from "express";
// 使用依赖的类型
function middleware(req: Request, res: Response): void;
export { middleware };
}
如何保持与源库的同步?
- 自动化工具:使用dts-gen生成基础声明。
- 版本控制:声明文件版本与源库版本保持一致。
- 持续集成:测试类型声明与最新版本兼容性。
- 社区协作:鼓励用户提交类型改进。
结语
声明文件是 TypeScript 生态系统的关键部分,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!