MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript第三方库类型定义开发规范

2021-08-014.4k 阅读

一、命名规范

  1. 文件命名 在TypeScript第三方库类型定义开发中,文件命名应遵循简洁明了且具有描述性的原则。通常情况下,类型定义文件的命名应与对应的JavaScript库名保持一致,并以 .d.ts 作为文件扩展名。例如,如果有一个JavaScript库名为 lodash,那么其类型定义文件应命名为 lodash.d.ts

对于一些子模块或特定功能的类型定义,可以采用在主库名后添加子模块名的方式,以 -/ 分隔。比如,lodash 库中的 cloneDeep 功能的类型定义文件可以命名为 lodash - cloneDeep.d.ts 或者 lodash/cloneDeep.d.ts(在符合项目目录结构规范的前提下)。

// 假设这是lodash - cloneDeep.d.ts的内容示例
declare function cloneDeep<T>(value: T): T;
  1. 类型命名
    • 接口命名:接口名应采用大驼峰命名法(PascalCase),并且名字要能够清晰地描述该接口所代表的数据结构或行为。例如,定义一个表示用户信息的接口:
interface UserInfo {
    name: string;
    age: number;
    email: string;
}
- **类型别名命名**:类型别名同样使用大驼峰命名法。当需要为复杂类型定义一个更简洁的名称时,类型别名就非常有用。比如,定义一个函数类型别名来表示具有特定参数和返回值的函数:
type AddFunction = (a: number, b: number) => number;
- **枚举命名**:枚举名也遵循大驼峰命名法。枚举成员使用全大写字母,单词之间用下划线分隔。例如,定义一个表示一周中各天的枚举:
enum Weekday {
    MONDAY = 'MON',
    TUESDAY = 'TUE',
    WEDNESDAY = 'WED',
    THURSDAY = 'THU',
    FRIDAY = 'FRI',
    SATURDAY = 'SAT',
    SUNDAY = 'SUN'
}

二、模块与导出规范

  1. 模块定义 TypeScript支持ES6模块系统,在定义第三方库类型定义时,应优先使用ES6模块的语法。每个类型定义文件应作为一个独立的模块。如果一个库有多个相关的类型定义,可以将它们组织在不同的文件中,然后通过 exportimport 进行管理。

例如,假设我们有一个 math - utils 库,包含加法和乘法两个功能的类型定义。我们可以创建两个文件 add.d.tsmultiply.d.ts

add.d.ts

export declare function add(a: number, b: number): number;

multiply.d.ts

export declare function multiply(a: number, b: number): number;

然后在一个主类型定义文件 math - utils.d.ts 中统一导出:

export * from './add.d.ts';
export * from './multiply.d.ts';
  1. 导出内容的组织
    • 导出顺序:按照一定的逻辑顺序导出内容。通常先导出类型定义(如接口、类型别名、枚举),然后再导出函数、类等可执行代码的定义。例如:
// 定义类型
interface Options {
    flag: boolean;
    value: string;
}
type Callback = () => void;

// 导出类型
export { Options, Callback };

// 定义函数
export declare function doSomething(options: Options, callback: Callback): void;
- **默认导出与命名导出**:对于只有一个主要导出内容(如一个类或一个主要函数)的模块,可以使用默认导出。例如:
export default function mainFunction(): void {
    console.log('This is the main function');
}

如果模块有多个导出内容,且没有明显的主要内容,则使用命名导出。这样在导入时可以更明确地选择需要的内容。

export function function1(): void {
    console.log('Function 1');
}

export function function2(): void {
    console.log('Function 2');
}

三、类型定义规范

  1. 基础类型定义
    • 原始类型:TypeScript对JavaScript的原始类型有直接的支持,如 stringnumberbooleannullundefinedsymbolbigint。在类型定义中,应准确使用这些原始类型。例如,定义一个接收字符串并返回布尔值的函数:
export declare function isStringNotEmpty(str: string): boolean;
- **数组类型**:有两种方式定义数组类型。一种是使用 `类型[]` 的语法,另一种是使用 `Array<类型>` 的泛型语法。例如:
// 定义一个字符串数组
export declare function processStringArray(arr: string[]): void;
// 定义一个数字数组
export declare function processNumberArray(arr: Array<number>): void;
  1. 对象类型定义
    • 接口方式:通过接口定义对象的形状。接口可以描述对象的属性和方法。属性可以有可选属性和只读属性。例如:
interface Person {
    name: string;
    age?: number; // 可选属性
    readonly id: number; // 只读属性
    sayHello(): void;
}
- **类型别名方式**:类型别名也可以定义对象类型,在某些情况下更加灵活。例如:
type Point = {
    x: number;
    y: number;
};
  1. 函数类型定义
    • 参数类型与返回值类型:明确函数的参数类型和返回值类型。例如,定义一个加法函数:
export declare function addNumbers(a: number, b: number): number;
- **可选参数与默认参数**:函数参数可以是可选的,通过在参数名后添加 `?` 表示。默认参数可以直接在函数定义中指定。例如:
export declare function greet(name: string, greeting: string = 'Hello'): void;
- **剩余参数**:使用 `...` 语法定义剩余参数,剩余参数类型通常是数组。例如:
export declare function sumNumbers(...nums: number[]): number;
  1. 泛型类型定义
    • 泛型函数:当函数的参数或返回值类型不固定,需要根据调用时传入的类型来确定时,使用泛型。例如,定义一个通用的数组映射函数:
export declare function mapArray<T, U>(arr: T[], callback: (item: T) => U): U[];
- **泛型接口**:接口也可以使用泛型。比如,定义一个通用的可获取值的接口:
interface Getter<T> {
    get(): T;
}
- **泛型约束**:有时需要对泛型类型进行约束,使其满足一定的条件。例如,定义一个泛型函数,要求传入的类型必须有 `length` 属性:
export declare function printLength<T extends { length: number }>(obj: T): void;

四、类型兼容性与声明合并

  1. 类型兼容性 TypeScript的类型兼容性基于结构类型系统。简单来说,如果类型 A 的结构能满足类型 B 的使用场景,那么 AB 兼容。

例如,接口 A 和接口 B

interface A {
    name: string;
}

interface B {
    name: string;
    age: number;
}

let a: A = { name: 'John' };
let b: B = { name: 'John', age: 30 };

a = b; // 允许,因为B的结构包含A的结构
// b = a; // 不允许,因为A缺少B中的age属性

在函数参数类型兼容性方面,如果函数 A 的参数类型能接受函数 B 的参数类型,那么函数 A 与函数 B 的参数类型兼容。例如:

function funcA(callback: (a: string) => void) {
    callback('Hello');
}

function funcB(callback: (a: string | number) => void) {
    callback(123);
    callback('World');
}

funcA(funcB); // 允许,因为string类型是string | number类型的子类型
  1. 声明合并 TypeScript支持声明合并,这在定义第三方库类型定义时非常有用。当有多个同名的声明时,TypeScript会将它们合并为一个声明。

接口合并:多个同名接口会合并它们的成员。例如:

interface User {
    name: string;
}

interface User {
    age: number;
}

let user: User = { name: 'Tom', age: 25 };

函数重载合并:可以通过函数重载来定义多个同名但参数不同的函数。例如:

export declare function formatDate(date: Date): string;
export declare function formatDate(date: string): string;

export function formatDate(date: Date | string): string {
    if (date instanceof Date) {
        return date.toISOString();
    } else {
        return new Date(date).toISOString();
    }
}

五、文档化规范

  1. JSDoc注释 使用JSDoc注释来为类型定义添加文档。JSDoc注释可以描述函数、接口、类型别名等的用途、参数、返回值等信息。

函数注释:对于函数,应注释其功能、参数的含义和类型、返回值的类型和含义。例如:

/**
 * 计算两个数字的和
 * @param a 第一个数字
 * @param b 第二个数字
 * @returns 两个数字的和
 */
export declare function add(a: number, b: number): number;

接口注释:对于接口,应注释接口所代表的数据结构或行为的含义。例如:

/**
 * 表示用户信息的接口
 * 包含用户的姓名、年龄和邮箱
 */
interface UserInfo {
    name: string;
    age: number;
    email: string;
}

类型别名注释:对于类型别名,注释其代表的复杂类型的含义。例如:

/**
 * 表示一个回调函数类型
 * 该回调函数无参数且无返回值
 */
type Callback = () => void;
  1. README文件 在类型定义项目的根目录下,应包含一个 README.md 文件。该文件应介绍类型定义所对应的JavaScript库的基本信息,如何安装和使用类型定义,以及一些特殊说明或注意事项。

例如:

# [库名] TypeScript类型定义

本项目提供了[库名] JavaScript库的TypeScript类型定义。

## 安装
使用npm安装:
```bash
npm install --save-dev [库名]@types

使用

在你的TypeScript项目中,引入[库名]库后,TypeScript会自动识别并使用本类型定义。例如:

import { function1 } from '[库名]';
function1();

注意事项

  1. 某些功能可能需要特定版本的[库名]库才能正常使用类型定义。
  2. 如果发现类型定义有问题,请在本项目的GitHub仓库中提交issue。

### 六、测试规范
1. **单元测试**
对于类型定义中的函数、类等可执行代码,应编写单元测试。可以使用测试框架如 `jest` 或 `mocha` 来编写测试用例。

例如,对于一个 `add` 函数的类型定义:
```typescript
// add.d.ts
export declare function add(a: number, b: number): number;

使用 jest 编写测试用例:

import { add } from './add';

test('add function should return the sum of two numbers', () => {
    expect(add(2, 3)).toBe(5);
});
  1. 类型测试 除了单元测试,还应进行类型测试,确保类型定义的正确性。可以使用工具如 @typescript - eslinttsd

@typescript - eslint:通过配置规则,可以检查类型定义中的潜在错误,如未使用的类型、错误的类型注解等。例如,在 .eslintrc.json 中配置:

{
    "extends": "@typescript - eslint/recommended",
    "parser": "@typescript - eslint/parser",
    "parserOptions": {
        "project": "./tsconfig.json"
    },
    "rules": {
        "@typescript - eslint/no - unused - variables": "error"
    }
}

tsdtsd 可以通过运行实际的TypeScript代码来测试类型定义。首先,安装 tsd

npm install --save - dev tsd

然后创建一个测试文件,如 test.ts

import { add } from './add';

let result: number = add(1, 2);

运行 tsd 进行类型测试:

npx tsd

七、发布与维护规范

  1. 发布 将类型定义发布到 @types 仓库是让其被广泛使用的常见方式。要发布到 @types 仓库,需要遵循以下步骤:

    • 确保类型定义遵循了前面提到的所有规范,包括命名、模块、类型定义、文档化和测试等。
    • 在类型定义项目的根目录下,运行 npm publish。注意,发布前应确保 package.json 文件中的信息准确,包括版本号、描述等。
    • 提交PR到 DefinitelyTyped 仓库(这是 @types 的主要代码仓库)。在PR中,应详细说明类型定义的内容、更新点等信息,以便维护者审核。
  2. 维护

    • 版本管理:随着JavaScript库的更新,类型定义也需要相应更新。应遵循语义化版本号规范(SemVer),即 MAJOR.MINOR.PATCH。当类型定义有不兼容的变更时,增加 MAJOR 版本号;当有向下兼容的新功能添加时,增加 MINOR 版本号;当有修复类型定义错误等补丁时,增加 PATCH 版本号。
    • 处理反馈:关注 @types 仓库或项目自身的GitHub仓库中的issue和PR。及时处理用户反馈的类型定义问题,如错误的类型、缺失的定义等。积极与社区互动,共同维护类型定义的质量。

通过遵循以上TypeScript第三方库类型定义开发规范,可以提高类型定义的质量、可维护性和易用性,为使用相关JavaScript库的TypeScript开发者提供更好的开发体验。