当我们在使用 TypeScript 开发时,经常需要安装 @types/xxx 来获得第三方库的类型支持。这些神秘的 @types 包是如何工作的?DefinitelyTyped 又是什么?本篇文章将揭开这些类型包的神秘面纱。
DefinitelyTyped:TypeScript的"类型宝库"
什么是DefinitelyTyped?
DefinitelyTyped(简称DT),是 GitHub 上的一个开源项目,专门为 JavaScript 库提供高质量的 TypeScript 类型定义。它是 TypeScript 生态系统中最重要的基础设施之一。
举个例子,比如我们正在使用一个纯 JavaScript 库,例如 lodash.js ,这里面是没有类型信息的。在 JavaScript 中是可以工作的:_.map([1,2,3], x => x * 2); 。
但在 TypeScript 中:import _ from 'lodash';,就会报错:找不到模块'lodash'的声明文件 。这时我们就需要安装类型定义:npm install --save-dev @types/lodash ,然后 TypeScript 就能理解 lodash 了。
类型包是如何创建的?
假设我们有一个原始的 JavaScript 库:tiny-validator.js:
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validatePhone(phone) {
return /^\d{10,11}$/.test(phone);
}
function validateURL(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
module.exports = {
validateEmail,
validatePhone,
validateURL,
version: '1.0.0'
};
现在我们要为它创建类型:
// 导出类型
export interface ValidationResult {
isValid: boolean;
message?: string;
}
// 导出函数
export function validateEmail(email: string): boolean;
export function validateEmail(email: string, strict: true): ValidationResult;
export function validatePhone(phone: string): boolean;
export function validatePhone(phone: string, countryCode: string): boolean;
export function validateURL(url: string): boolean;
export function validateURL(url: string, options: { requireProtocol: boolean }): boolean;
// 导出常量
export const version: string;
// 默认导出
declare const validator: {
validateEmail: typeof validateEmail;
validatePhone: typeof validatePhone;
validateURL: typeof validateURL;
version: typeof version;
};
export default validator;
@types包的工作原理
类型包的发布流程
假设我们要发布一个 @types/react 包:
- 在DefinitelyTyped提交PR,提交更改到
types/react/;
- CI运行测试:
- 维护者审查
- 合并PR
- 自动发布到 npm 为 @types/react
类型包如何被TypeScript识别
TypeScript 会按以下顺序查找类型(以 lodash 为例):
- 当前文件的 .d.ts 声明
- 项目中的 .d.ts 文件
- node_modules/@types/lodash/index.d.ts:DefinitelyTyped提供的
- node_modules/lodash/package.json 中的 "types" 字段
- node_modules/lodash/index.d.ts:库自带的类型
当然,我们也可以通过配置 tsconfig.json,影响相关的顺序:
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types", // 默认包含
"./custom-types" // 自定义类型目录
],
"types": [ // 指定要包含的类型包
"node",
"lodash",
"react"
]
}
}
版本管理:@types包的命名规则
@types包的版本与对应库的版本关联,命名规则为:@types/<库名> ,可以看下面的示例:
| 原始库 |
类型包 |
说明 |
| lodash |
@types/lodash |
与lodash主版本对应 |
| react |
@types/react |
与react版本对应 |
| node |
@types/node |
与Node.js版本对应 |
当然,也存在特殊情况:如带作用域的包:@angular/core → @types/angular__core 。
类型包的版本管理
@types/node的版本管理
@types/node 是所有 @types 包中最特殊的一个,因为它与Node.js运行时版本紧密绑定。
// package.json 中的依赖
{
"devDependencies": {
// @types/node的版本应该与你的Node.js版本匹配
"@types/node": "^18.0.0", // 对应Node.js 18.x
// 其他类型包
"@types/express": "^4.17.0",
"@types/lodash": "^4.14.0"
}
}
// @types/node的版本演进示例
// Node.js 16.x 的类型
declare module "fs/promises" {
function readFile(path: string): Promise<Buffer>;
// Node 16的API
}
// Node.js 18.x 新增了fetch API
declare global {
// Node 18新增了全局fetch
function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
interface Request {}
interface Response {}
}
这就是为什么我们在开发时需要:
- Node.js 16 → @types/node@16.x
- Node.js 18 → @types/node@18.x
- Node.js 20 → @types/node@20.x
类型包的版本兼容性
类型包版本不匹配/不兼容的问题,应该是我们在使用类型包时遇到的最常见也是最麻烦的问题。为什么会出现这样的问题呢?例如我们的项目中使用 lodash@4.17.21 ,但实际上 @types/lodash 已经更新到了新版本,这样就出现了版本兼容性问题。
解决方案
指定精确版本
npm install @types/lodash@4.14.0 # 安装特定版本
查看库的package.json
{
"name": "my-library",
"version": "2.0.0",
"devDependencies": {
"@types/lodash": "^4.14.0" # 推荐的类型版本
}
}
使用peerDependencies
{
"name": "@types/my-library",
"peerDependencies": {
"my-library": ">=2.0.0 <3.0.0" # 要求原库版本
}
}
如何选择合适的@types版本
1. 检查你安装的库版本
// package.json
{
"dependencies": {
"react": "17.0.2", // React 17.0.2
"lodash": "4.17.21" // Lodash 4.17.21
}
}
2. 查找对应的@types版本
例如:
对于React 18,应该使用 @types/react@18.x.x
对于Lodash 4,应该使用 @types/lodash@4.x.x
3. 查看版本历史
可以通过npm查看版本信息:
npm view @types/react versions # 查看所有版本
npm view @types/react@18.0.0 # 查看特定版本信息
4. 在package.json中指定
{
"devDependencies": {
"@types/react": "17.0.0", // 匹配React 17.0.x
"@types/lodash": "4.14.0" // Lodash 4的类型
}
}
5. 处理冲突
如果库A依赖 @types/lodash@4.14.0,而库B依赖 @types/lodash@4.17.0 ,TypeScript会自动选择较高的版本(4.17.0)。这在大多数情况下是安全的,因为类型定义是向后兼容的。
常见问题
多个类型包冲突
场景:两个库都提供了相同的类型定义,例如:
在 @types/jquery 和 @types/some-other-plugin 两个包中都声明了全局的 $,导致冲突!
解决方案
1. 使用模块声明而不是全局声明
declare module "jquery-plugin" {
import $ from "jquery";
// 使用导入的$,而不是声明全局的$
interface JQuery {
myPlugin(): JQuery;
}
}
2. 合并声明
如果两个声明不冲突,可以共存:
interface JQuery {
// 来自jquery
hide(): JQuery;
show(): JQuery;
// 来自plugin1
plugin1Method(): JQuery;
// 来自plugin2
plugin2Method(): JQuery;
}
3. 在tsconfig中排除有问题的类型
{
"compilerOptions": {
"types": [
"jquery", // 包含jquery
// "jquery-plugin" // 排除冲突的插件类型
]
}
}
类型包版本不匹配
场景:类型包版本与库版本不匹配,例如库版本为 lodash@4.17.21 ,而类型包版本为 @types/lodash@4.14.0 。由于类型包版本较旧,导致缺失某个API,直接使用新版API会报错。
解决方案
1. 升级类型包
npm update @types/lodash@latest
2. 临时补充类型定义
// custom-types/lodash-extensions.d.ts
import 'lodash';
declare module 'lodash' {
interface LoDashStatic {
// 补充缺失的方法
flattenDepth<T>(array: ListOfRecursiveArraysOrValues<T>, depth?: number): T[];
}
}
3. 降级库版本(不推荐)
npm install lodash@4.14.0 # 与类型包匹配
全局类型污染
场景:类型包声明了全局类型,影响了我们的代码。例如:我们在使用 @types/jest,它声明了全局的 describe、it、expect 。但在我们的非测试代码中,这些是不应该存在的。
解决方案
1. 隔离测试类型
// 在 tsconfig.json 中
{
"extends": "./tsconfig.base.json",
// 主配置:排除测试类型
"compilerOptions": {
"types": [] // 不包含任何全局类型
},
// 测试配置
"tsconfig.test.json": {
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"] // 只在测试中包含
}
}
}
2. 使用import代替全局
import { describe, it, expect } from '@jest/globals';
describe('test', () => {
it('should work', () => {
expect(1 + 1).toBe(2);
});
});
循环依赖的类型包
现象:类型包相互依赖导致循环。例如:@types/react 依赖于 @types/prop-types;而 @types/prop-types 反过来又可能间接引用 @types/react 。结果就导致:导入循环或最大深度超出。
解决方案
1. 使用 import type
即:在类型包中,只导入类型,不导入值:
import type { ReactNode } from 'react';
2. 使用三斜线引用
/// <reference types="react" /> // 这告诉TypeScript类型存在,但不导入
在 ES6 以前,JavaScript 并没有一个官方的模块系统,而是在不同的环境中,以不同的方式加上了这个缺失的特性。如 node.js 使用 require 和 module.exports;AMD 使用一个带回调的 define 函数。后来,TypeScript 通过 module 关键字和“三斜线”导入来填补了这个空白。
module 关键字和“三斜线”导入只是一个历史遗迹,在 ES6+ 中,还是推荐 import 和 export 。
3. 重新设计类型结构
即:将共享类型提取到单独的文件:
// types/shared/index.d.ts
export interface CommonProps {
className?: string;
style?: React.CSSProperties;
}
// types/react/index.d.ts
import { CommonProps } from '../shared';
// types/vue/index.d.ts
import { CommonProps } from '../shared';
最佳实践指南
作为类型包使用者
package.json 最佳配置
// package.json 最佳配置
{
"name": "my-app",
"version": "1.0.0",
"devDependencies": {
// 1. 匹配运行时版本
"@types/node": "^18.0.0", // 与Node.js版本匹配
// 2. 使用^允许次要版本更新
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
// 3. 按需安装,不要一次性安装所有@types
// 错误做法:@types/* (安装所有)
// 正确做法:只安装需要的
// 4. 定期更新
// npm update @types/*
},
"scripts": {
// 5. 添加类型检查脚本
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch"
}
}
tsconfig.json 最佳配置
// tsconfig.json 最佳实践
{
"compilerOptions": {
// 1. 明确指定类型包
"types": [
"node",
"react",
"react-dom"
// 明确列出,避免意外包含
],
// 2. 控制类型查找路径
"typeRoots": [
"./node_modules/@types",
"./src/types" // 自定义类型目录
],
// 3. 启用严格检查
"strict": true,
// 4. 处理模块解析
"moduleResolution": "node",
// 5. 确保兼容性
"skipLibCheck": true, // 跳过库的类型检查,提高编译速度
"forceConsistentCasingInFileNames": true
},
// 6. 包含和排除文件
"include": [
"src/**/*",
"tests/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
作为类型包贡献者
1. 使用函数重载而不是联合类型
// ❌ 不好
function parse(input: string | number): Date;
// ✅ 更好
function parse(input: string): Date;
function parse(input: number): Date;
2. 使用泛型提高灵活性
// ❌ 不灵活、不安全
function getValue(obj: any, key: string): any;
// ✅ 类型安全
function getValue<T, K extends keyof T>(obj: T, key: K): T[K];
3. 提供完整的JSDoc注释
/**
* 验证电子邮件地址
* @param email - 要验证的电子邮件地址
* @param strict - 是否进行严格验证
* @returns 验证结果
* @example
* validateEmail('test@example.com') // true
*/
export function validateEmail(email: string, strict?: boolean): boolean;
4. 包含类型测试
import _ = require('lodash');
// 测试类型推断
const numbers: number[] = [1, 2, 3];
const doubled = _.map(numbers, x => x * 2); // 应该推断为 number[]
// 测试边界情况
_.chunk([], 2); // 应该返回 [] 而不是 never[]
5. 遵循命名约定
// 使用帕斯卡命名法(PascalCase)表示类型和接口
interface UserConfig { /* ... */ }
type ValidationResult = { /* ... */ };
// 使用驼峰命名法(camelCase)表示函数和变量
function validateInput(input: string): boolean;
const defaultConfig: UserConfig = { /* ... */ };
TypeScript的演进
TypeScript正在减少对@types的依赖
- 声明文件自动生成:使用
tsc --declaration 自动生成 .d.ts 文件
- 更好的模块解析:TypeScript 4.7+ 改进了对
package.json 中 exports 的支持
- 类型导入优化:
import type 语法,减少运行时依赖
- 部分类型检查:可以对 JavaScript 文件进行渐进式类型检查
开发者建议
库开发者:
- 用TypeScript编写你的库
- 发布时包含类型定义
- 使用严格类型检查
- 提供完整的API文档
应用开发者:
- 优先选择有自带类型的库
- 定期更新@types包
- 学会阅读和理解类型定义
- 为缺失类型的库贡献类型定义
类型维护者:
- 保持与上游库的同步
- 编写全面的类型测试
- 及时响应社区问题
- 遵循DefinitelyTyped的贡献指南
结语
@types 生态系统是 TypeScript 成功的关键因素之一。通过 DefinitelyTyped 项目,社区为成千上万的 JavaScript 库提供了高质量的类型定义。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!