@types 包的工作原理与最佳实践

@types 包的工作原理与最佳实践

当我们在使用 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

javascript 复制代码
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'
};

现在我们要为它创建类型:

typescript 复制代码
// 导出类型
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 包:

  1. 在DefinitelyTyped提交PR,提交更改到 types/react/
  2. CI运行测试:
    • 编译类型检查
    • 运行类型测试
    • 验证格式
  3. 维护者审查
  4. 合并PR
  5. 自动发布到 npm 为 @types/react

类型包如何被TypeScript识别

TypeScript 会按以下顺序查找类型(以 lodash 为例):

  1. 当前文件的 .d.ts 声明
  2. 项目中的 .d.ts 文件
  3. node_modules/@types/lodash/index.d.ts:DefinitelyTyped提供的
  4. node_modules/lodash/package.json 中的 "types" 字段
  5. node_modules/lodash/index.d.ts:库自带的类型

当然,我们也可以通过配置 tsconfig.json,影响相关的顺序:

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运行时版本紧密绑定。

json 复制代码
// 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"
  }
}
typescript 复制代码
// @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 已经更新到了新版本,这样就出现了版本兼容性问题。

解决方案

指定精确版本
bash 复制代码
npm install @types/lodash@4.14.0  # 安装特定版本
查看库的package.json
typescript 复制代码
{
  "name": "my-library",
  "version": "2.0.0",
  "devDependencies": {
    "@types/lodash": "^4.14.0"  # 推荐的类型版本
  }
}
使用peerDependencies
typescript 复制代码
{
  "name": "@types/my-library",
  "peerDependencies": {
    "my-library": ">=2.0.0 <3.0.0"  # 要求原库版本
  }
}

如何选择合适的@types版本

1. 检查你安装的库版本

json 复制代码
// 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查看版本信息:

bash 复制代码
npm view @types/react versions  # 查看所有版本
npm view @types/react@18.0.0    # 查看特定版本信息

4. 在package.json中指定

typescript 复制代码
{
  "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. 使用模块声明而不是全局声明
typescript 复制代码
declare module "jquery-plugin" {
  import $ from "jquery";
  
  // 使用导入的$,而不是声明全局的$
  interface JQuery {
    myPlugin(): JQuery;
  }
}
2. 合并声明

如果两个声明不冲突,可以共存:

typescript 复制代码
interface JQuery {
  // 来自jquery
  hide(): JQuery;
  show(): JQuery;
  
  // 来自plugin1
  plugin1Method(): JQuery;
  
  // 来自plugin2  
  plugin2Method(): JQuery;
}
3. 在tsconfig中排除有问题的类型
typescript 复制代码
{
  "compilerOptions": {
    "types": [
      "jquery",           // 包含jquery
      // "jquery-plugin"   // 排除冲突的插件类型
    ]
  }
}

类型包版本不匹配

场景:类型包版本与库版本不匹配,例如库版本为 lodash@4.17.21 ,而类型包版本为 @types/lodash@4.14.0 。由于类型包版本较旧,导致缺失某个API,直接使用新版API会报错。

解决方案

1. 升级类型包
bash 复制代码
npm update @types/lodash@latest
2. 临时补充类型定义
typescript 复制代码
// custom-types/lodash-extensions.d.ts
import 'lodash';

declare module 'lodash' {
  interface LoDashStatic {
    // 补充缺失的方法
    flattenDepth<T>(array: ListOfRecursiveArraysOrValues<T>, depth?: number): T[];
  }
}
3. 降级库版本(不推荐)
bash 复制代码
npm install lodash@4.14.0  # 与类型包匹配

全局类型污染

场景:类型包声明了全局类型,影响了我们的代码。例如:我们在使用 @types/jest,它声明了全局的 describeitexpect 。但在我们的非测试代码中,这些是不应该存在的。

解决方案

1. 隔离测试类型
typescript 复制代码
// 在 tsconfig.json 中
{
  "extends": "./tsconfig.base.json",
  // 主配置:排除测试类型
  "compilerOptions": {
    "types": []  // 不包含任何全局类型
  },
  
  // 测试配置
  "tsconfig.test.json": {
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "types": ["jest", "node"]  // 只在测试中包含
    }
  }
}
2. 使用import代替全局
typescript 复制代码
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

即:在类型包中,只导入类型,不导入值:

typescript 复制代码
import type { ReactNode } from 'react';
2. 使用三斜线引用
typescript 复制代码
/// <reference types="react" /> // 这告诉TypeScript类型存在,但不导入

在 ES6 以前,JavaScript 并没有一个官方的模块系统,而是在不同的环境中,以不同的方式加上了这个缺失的特性。如 node.js 使用 requiremodule.exports;AMD 使用一个带回调的 define 函数。后来,TypeScript 通过 module 关键字和"三斜线"导入来填补了这个空白。
module 关键字和"三斜线 "导入只是一个历史遗迹,在 ES6+ 中,还是推荐 importexport

3. 重新设计类型结构

即:将共享类型提取到单独的文件:

typescript 复制代码
// 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 最佳配置

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 最佳配置

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. 使用函数重载而不是联合类型

typescript 复制代码
// ❌ 不好
function parse(input: string | number): Date;

// ✅ 更好
function parse(input: string): Date;
function parse(input: number): Date;

2. 使用泛型提高灵活性

typescript 复制代码
// ❌ 不灵活、不安全
function getValue(obj: any, key: string): any;

// ✅ 类型安全
function getValue<T, K extends keyof T>(obj: T, key: K): T[K];

3. 提供完整的JSDoc注释

typescript 复制代码
/**
 * 验证电子邮件地址
 * @param email - 要验证的电子邮件地址
 * @param strict - 是否进行严格验证
 * @returns 验证结果
 * @example
 * validateEmail('test@example.com') // true
 */
export function validateEmail(email: string, strict?: boolean): boolean;

4. 包含类型测试

typescript 复制代码
import _ = require('lodash');

// 测试类型推断
const numbers: number[] = [1, 2, 3];
const doubled = _.map(numbers, x => x * 2);  // 应该推断为 number[]

// 测试边界情况
_.chunk([], 2);  // 应该返回 [] 而不是 never[]

5. 遵循命名约定

typescript 复制代码
// 使用帕斯卡命名法(PascalCase)表示类型和接口
interface UserConfig { /* ... */ }
type ValidationResult = { /* ... */ };

// 使用驼峰命名法(camelCase)表示函数和变量
function validateInput(input: string): boolean;
const defaultConfig: UserConfig = { /* ... */ };

TypeScript的演进

TypeScript正在减少对@types的依赖

  1. 声明文件自动生成:使用 tsc --declaration 自动生成 .d.ts 文件
  2. 更好的模块解析:TypeScript 4.7+ 改进了对 package.jsonexports 的支持
  3. 类型导入优化:import type 语法,减少运行时依赖
  4. 部分类型检查:可以对 JavaScript 文件进行渐进式类型检查

开发者建议

库开发者:

  1. 用TypeScript编写你的库
  2. 发布时包含类型定义
  3. 使用严格类型检查
  4. 提供完整的API文档

应用开发者:

  1. 优先选择有自带类型的库
  2. 定期更新@types包
  3. 学会阅读和理解类型定义
  4. 为缺失类型的库贡献类型定义

类型维护者:

  1. 保持与上游库的同步
  2. 编写全面的类型测试
  3. 及时响应社区问题
  4. 遵循DefinitelyTyped的贡献指南

结语

@types 生态系统是 TypeScript 成功的关键因素之一。通过 DefinitelyTyped 项目,社区为成千上万的 JavaScript 库提供了高质量的类型定义。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
我是伪码农2 小时前
Vue 1.27
前端·javascript·vue.js
秋名山大前端2 小时前
前端大规模 3D 轨迹数据可视化系统的性能优化实践
前端·3d·性能优化
H7998742422 小时前
2026动态捕捉推荐:8款专业产品全方位测评
大数据·前端·人工智能
ct9782 小时前
Cesium 矩阵系统详解
前端·线性代数·矩阵·gis·webgl
小陈phd2 小时前
langGraph从入门到精通(十一)——基于langgraph构建复杂工具应用的ReAct自治代理
前端·人工智能·react.js·自然语言处理
我要敲一万行2 小时前
前端面试erp项目常问问题
前端·面试
2 小时前
ubuntu 通过ros-noetic获取RTK模块的nmea格式数据
java·前端·javascript
雨季6662 小时前
构建 OpenHarmony 简易密码强度指示器:用字符串长度实现直观反馈
android·开发语言·javascript
&活在当下&2 小时前
uniapp 选择城市区号索引列表实现
前端·uni-app