声明文件.d.ts:编写自己的类型定义

39 阅读1分钟

在 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 生态系统的关键部分,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!