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 {}?
没有 import 或 export 的文件被视为全局脚本,声明会成为全局声明。使用 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 声明文件:
- 基本语法:使用
declare声明变量、函数、类等 - 模块声明:为模块添加类型定义
- 模块扩展:扩展现有模块的类型
- 声明文件模板:不同类型库的声明模式
- 高级模式:可调用对象、可构造对象等
- 最佳实践:编写高质量声明文件的建议
- 发布声明文件:三种发布方式
练习
- 为一个简单的 JavaScript 工具库编写声明文件
- 扩展 Express 的 Request 和 Response 接口
- 为没有类型的 npm 包创建本地声明文件
- 创建一个全局声明文件,添加自定义的 String 方法