跳到主要内容

TypeScript 声明文件

声明文件(.d.ts)用于描述 JavaScript 代码的类型信息,让 TypeScript 能够理解 JavaScript 库的 API。当使用没有 TypeScript 类型的 JavaScript 库时,声明文件非常重要。

什么是声明文件

声明文件只包含类型声明,不包含实现代码,扩展名为 .d.ts

// utils.d.ts
declare function greet(name: string): string;

declare const version: string;

declare interface User {
id: number;
name: string;
}

解释declare 关键字告诉 TypeScript 这些声明存在于运行时,但不需要编译输出。

基本语法

声明变量

// global.d.ts
declare const API_URL: string;
declare let debug: boolean;
declare var window: Window;

声明函数

// utils.d.ts
declare function fetch(url: string): Promise<Response>;
declare function log(message: string): void;

声明类

// models.d.ts
declare class User {
constructor(id: number, name: string);
id: number;
name: string;
greet(): string;
}

声明枚举

declare enum Direction {
Up,
Down,
Left,
Right
}

模块声明

为模块添加类型

// my-library.d.ts
declare module "my-library" {
export function doSomething(value: string): number;

export interface Config {
timeout: number;
retries: number;
}

export const version: string;

export default class MyLibrary {
constructor(config: Config);
execute(): void;
}
}

使用:

import MyLibrary, { doSomething, Config } from "my-library";

const config: Config = { timeout: 1000, retries: 3 };
const lib = new MyLibrary(config);

模块扩展

扩展现有模块的类型:

// express-extension.d.ts
import "express";

declare module "express" {
interface Request {
// 添加自定义属性
user?: {
id: string;
name: string;
};
}

interface Response {
// 添加自定义方法
success(data: any): void;
}
}

使用:

import express from "express";

const app = express();

app.use((req, res, next) => {
req.user = { id: "1", name: "张三" }; // TypeScript 知道这个属性
next();
});

app.get("/", (req, res) => {
res.success({ message: "OK" }); // TypeScript 知道这个方法
});

全局扩展

// global.d.ts
declare global {
interface Window {
myApp: {
version: string;
config: Record<string, unknown>;
};
}

// 声明全局变量
const ENV: "development" | "production";
}

export {}; // 使文件成为模块

声明文件模板

全局变量库

// global-library.d.ts

// 全局函数
declare function myLib(name: string): void;

// 全局对象
declare namespace myLib {
function greet(name: string): string;

interface Config {
theme: "light" | "dark";
lang: string;
}

const version: string;
}

UMD 库

// umd-library.d.ts
export as namespace myLib;

export function greet(name: string): string;

export interface Config {
timeout: number;
}

模块插件

// module-plugin.d.ts
import "some-library";

declare module "some-library" {
interface SomeInterface {
myPluginMethod(): void;
}
}

为 JavaScript 文件编写声明

场景:迁移 JavaScript 项目

假设有一个 JavaScript 文件:

// math-utils.js
function add(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

const PI = 3.14159;

module.exports = {
add,
subtract,
PI
};

创建对应的声明文件:

// math-utils.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;

export const PI: number;

场景:第三方库没有类型

// types/legacy-library/index.d.ts
declare module "legacy-library" {
interface Options {
debug?: boolean;
timeout?: number;
}

function init(options?: Options): void;

namespace LegacyLibrary {
interface User {
id: string;
name: string;
}

function getUser(id: string): User;
function createUser(data: Partial<User>): User;
}

export = LegacyLibrary;
}

高级声明模式

可调用对象

// 可作为函数调用,同时有属性
interface Greeter {
(name: string): string;
defaultName: string;
version: string;
}

declare const greet: Greeter;

使用:

greet("张三");  // 作为函数调用
console.log(greet.defaultName); // 访问属性

可构造对象

// 可以 new,也可以作为函数调用
interface SomeType {
new (value: string): SomeInstance;
(value: string): SomeInstance;
}

interface SomeInstance {
value: string;
method(): void;
}

declare const SomeType: SomeType;

复杂函数重载

// 多种调用方式
declare function createElement(tag: "a"): HTMLAnchorElement;
declare function createElement(tag: "canvas"): HTMLCanvasElement;
declare function createElement(tag: "table"): HTMLTableElement;
declare function createElement(tag: string): HTMLElement;

this 参数

interface DOMElement {
getText(this: DOMElement): string;
setText(this: DOMElement, text: string): void;
}

声明文件的最佳实践

1. 不要在声明文件中定义实现

// 错误:声明文件包含实现
declare function add(a: number, b: number): number {
return a + b; // 错误!
}

// 正确:只有类型声明
declare function add(a: number, b: number): number;

2. 使用精确的类型

// 不好的做法
declare function query(selector: string): any;

// 好的做法
declare function query(selector: string): Element | null;
declare function queryAll(selector: string): NodeListOf<Element>;

3. 使用命名空间组织类型

declare namespace MyLibrary {
interface User {
id: string;
name: string;
}

interface Product {
id: string;
name: string;
price: number;
}

namespace Utils {
function formatPrice(price: number): string;
}
}

4. 导出和默认导出

// 命名导出
export function foo(): void;
export const bar: string;

// 默认导出
export default function (): void;

// 或
declare function myLib(): void;
export = myLib;

5. 类型 vs 值

理解什么时候使用 type vs interface vs declare

// 声明类型(编译时)
export type UserID = string;

// 声明接口(编译时)
export interface User {
id: UserID;
name: string;
}

// 声明值(运行时存在)
declare const currentUser: User;
declare function getUser(id: UserID): User;

配置 TypeScript 查找声明文件

使用 typeRoots

{
"compilerOptions": {
"typeRoots": [
"./types",
"./node_modules/@types"
]
}
}

使用 paths

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"my-library": ["types/my-library"]
}
}
}

包含自定义类型目录

{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/types"]
},
"include": ["src/**/*", "types/**/*"]
}

发布声明文件

方式一:与 npm 包一起发布

// package.json
{
"name": "my-library",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"]
}

方式二:发布到 DefinitelyTyped

对于没有类型的第三方库,可以贡献到 DefinitelyTyped:

# 包名为 @types/library-name
npm install --save-dev @types/lodash

方式三:本地声明文件

在项目中创建 types 目录:

my-project/
├── types/
│ ├── my-library.d.ts
│ └── global.d.ts
├── src/
│ └── index.ts
└── tsconfig.json

实用示例

为 jQuery 插件添加类型

// types/jquery.myplugin.d.ts
import "jquery";

declare module "jquery" {
interface JQuery {
myPlugin(options?: {
speed?: number;
effect?: "fade" | "slide";
}): JQuery;
}
}

为 Node.js 扩展全局类型

// types/global.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
API_URL: string;
DATABASE_URL?: string;
}
}

为 Express 中间件添加类型

// types/express.d.ts
import "express";

declare module "express" {
interface Request {
session?: {
id: string;
user?: {
id: string;
role: "admin" | "user";
};
};
}
}

常见问题

Q: 为什么需要 export {}

没有 importexport 的文件被视为全局脚本,声明会成为全局声明。使用 export {} 将文件标记为模块:

// global.d.ts
declare global {
interface String {
capitalize(): string;
}
}

export {}; // 使文件成为模块

Q: declare module 和 declare namespace 的区别?

  • declare module:声明外部模块
  • declare namespace:声明全局命名空间或模块内部命名空间
// 声明模块
declare module "my-library" {
// 模块内容
}

// 声明全局命名空间
declare namespace MyLibrary {
// 命名空间内容
}

Q: 如何处理动态导入?

declare module "dynamic-module" {
export function init(): Promise<void>;
export const loaded: boolean;
}

// 使用
const module = await import("dynamic-module");

小结

本章我们学习了 TypeScript 声明文件:

  1. 基本语法:使用 declare 声明变量、函数、类等
  2. 模块声明:为模块添加类型定义
  3. 模块扩展:扩展现有模块的类型
  4. 声明文件模板:不同类型库的声明模式
  5. 高级模式:可调用对象、可构造对象等
  6. 最佳实践:编写高质量声明文件的建议
  7. 发布声明文件:三种发布方式

练习

  1. 为一个简单的 JavaScript 工具库编写声明文件
  2. 扩展 Express 的 Request 和 Response 接口
  3. 为没有类型的 npm 包创建本地声明文件
  4. 创建一个全局声明文件,添加自定义的 String 方法