JavaScript比较参数在TypeScript里类型怎么标

作者:小括号

时间:2026-11-08

浏览:136

详细讲解JavaScript中常见的比较参数(如sort的compareFn、自定义比较回调)在TypeScript里应该怎样标注类型,包括基础写法、泛型封装、常见易错点和实用示例。

文章目录

JavaScript 的 Array.prototype.sort()、Array.prototype.find()、自定义排序与搜索函数里,都会用到"比较参数"——即那些接收两个值并返回数字或布尔结果的回调函数。把这些比较参数搬进 TypeScript 时,需要给回调本身以及它的参数、返回值加上准确的类型标注。本文从最基础的写法讲起,逐步覆盖泛型封装、联合类型处理和常见踩坑点。

sort 的 compareFn 最基本的写法

sort 方法接收一个可选的比较函数,签名是 (a, b) => number。在 TypeScript 里,如果数组本身有明确的类型,比较函数的参数类型可以自动推断:

const nums: number[] = [3, 1, 4, 1, 5];
nums.sort((a, b) => a - b);
// a 和 b 自动推断为 number,无需手动标注

当你把比较函数提取成独立变量时,就需要显式标注了。有两种常见做法:

// 做法一:直接在参数上标类型
const compareNumbers = (a: number, b: number): number => a - b;

// 做法二:用函数类型声明
const compareNumbers2: (a: number, b: number) => number = (a, b) => a - b;

两种写法效果一样,做法一更直观,做法二在需要复用类型时更方便——可以先定义一个类型别名。

用类型别名统一管理比较函数类型

当项目里出现多个同类比较函数时,反复写 (a: number, b: number) => number 既冗长又不好改。标准做法是提取成类型别名:

type CompareFn<T> = (a: T, b: T) => number;

const byAge: CompareFn<{ age: number }> = (a, b) => a.age - b.age;
const byName: CompareFn<{ name: string }> = (a, b) => a.name.localeCompare(b.name);

这样做有三个好处:修改签名只需改一个地方;泛型参数 T 让同一类型适配不同数据;函数实现处不再需要重复写参数类型,TypeScript 会从 CompareFn<...> 推断出来。

JavaScript比较参数在TypeScript里类型怎么标
图1:比较函数类型标注从内联到泛型别名的四步演进

对象数组排序时的类型标注

实际项目中最常见的场景是对对象数组排序。这时候比较函数的参数类型就是对象的类型:

interface User {
  name: string;
  age: number;
  score: number;
}

const users: User[] = [
  { name: '张三', age: 28, score: 85 },
  { name: '李四', age: 22, score: 92 },
  { name: '王五', age: 25, score: 78 },
];

// 按年龄升序
users.sort((a: User, b: User): number => a.age - b.age);

// 用之前定义的 CompareFn 类型
const byScore: CompareFn<User> = (a, b) => b.score - a.score; // 降序
users.sort(byScore);

注意 sort 的比较函数必须返回 number。返回负数表示 a 排在 b 前,正数表示 b 排在前,零表示顺序不变。不要返回布尔值——虽然 JavaScript 不会报错,但 TypeScript 会提示类型不匹配,而且布尔转数字只有 0 和 1,排序结果不稳定。

处理可能为 null 或 undefined 的情况

当数组元素或要比较的属性可能为空时,类型标注需要考虑联合类型:

interface Product {
  name: string;
  price: number | null;
}

const byPrice: CompareFn<Product> = (a, b) => {
  // null 排在最后
  if (a.price === null && b.price === null) return 0;
  if (a.price === null) return 1;
  if (b.price === null) return -1;
  return a.price - b.price;
};

TypeScript 的控制流分析会在每个 if 分支之后自动收窄类型。经过前三个判断后,最后一行的 a.price 和 b.price 都被确认为 number,可以安全做减法。

JavaScript比较参数在TypeScript里类型怎么标
图2:包含 null 值时比较函数的判断与类型收窄流程

把比较函数作为高阶函数的参数

当你编写接收比较函数作为参数的工具函数时,需要在函数签名里标注回调的类型。结合泛型可以写出通用的排序工具:

function sortBy<T>(arr: T[], compareFn: (a: T, b: T) => number): T[] {
  return [...arr].sort(compareFn);
}

// 使用时 T 自动推断
const sorted = sortBy(users, (a, b) => a.age - b.age);
// sorted 的类型是 User[]

这里 T 由传入的数组决定,比较函数的参数类型跟着确定,调用方不需要手动传泛型参数。如果要做更复杂的操作,比如按某个 key 排序,可以进一步约束:

function sortByKey<T, K extends keyof T>(
  arr: T[],
  key: K,
  compareFn: (a: T[K], b: T[K]) => number
): T[] {
  return [...arr].sort((x, y) => compareFn(x[key], y[key]));
}

// 用法
sortByKey(users, 'age', (a, b) => a - b);
// a 和 b 被推断为 number(User['age'] 的类型)

这里 K extends keyof T 约束了 key 必须是 T 的属性名,T[K] 则取到该属性的值类型。比较函数的参数自动变成该属性的类型。

JavaScript比较参数在TypeScript里类型怎么标
图3:sortByKey 泛型函数中三个参数的类型推断链

字符串比较和 localeCompare 的返回值

String.prototype.localeCompare() 返回 number,可以直接用在比较函数里。但要注意它的类型声明返回的是 number 而不是 -1 | 0 | 1,所以和 CompareFn 是兼容的:

const byName: CompareFn<User> = (a, b) => a.name.localeCompare(b.name, 'zh-CN');

如果遇到需要先按一个字段排、再按另一个字段排的多级排序,可以这样组合:

const multiSort: CompareFn<User> = (a, b) => {
  const ageDiff = a.age - b.age;
  if (ageDiff !== 0) return ageDiff;
  return a.name.localeCompare(b.name, 'zh-CN');
};

常见的类型错误和修正

返回布尔值而非数字

这是从 JavaScript 迁移过来最常犯的错:

// ❌ 错误:返回 boolean,TypeScript 会报错
// Type 'boolean' is not assignable to type 'number'
const wrong: CompareFn<number> = (a, b) => a > b;

// ✅ 正确:返回数字
const right: CompareFn<number> = (a, b) => a - b;

忘记处理 undefined

在 strictNullChecks 开启的项目里,如果数组类型是 (number | undefined)[],比较函数参数也会包含 undefined:

const arr: (number | undefined)[] = [3, undefined, 1, undefined, 2];

arr.sort((a, b) => {
  // a 和 b 的类型是 number | undefined
  // 直接 a - b 会报错
  if (a === undefined) return 1;
  if (b === undefined) return -1;
  return a - b;
});

泛型参数过于宽泛

写 CompareFn<any> 虽然不报错,但失去了类型检查的意义。尽量给泛型一个具体的类型或至少一个约束:

// 不推荐
const loose: CompareFn<any> = (a, b) => a.foo - b.foo; // 不会检查 foo 是否存在

// 推荐
const strict: CompareFn<{ foo: number }> = (a, b) => a.foo - b.foo; // foo 必须存在且为 number

在非 sort 场景中标注比较参数

比较参数不只出现在 sort 里。自定义的二分搜索、优先队列、合并操作等数据结构和算法都需要比较函数。这些场景下的类型标注思路相同——关键是明确参数类型和返回值含义:

class PriorityQueue<T> {
  private items: T[] = [];
  private compareFn: (a: T, b: T) => number;

  constructor(compareFn: (a: T, b: T) => number) {
    this.compareFn = compareFn;
  }

  enqueue(item: T): void {
    this.items.push(item);
    this.items.sort(this.compareFn);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }
}

// 最小堆:数字小的优先
const minQueue = new PriorityQueue<number>((a, b) => a - b);
minQueue.enqueue(5);
minQueue.enqueue(2);
minQueue.enqueue(8);

这里 PriorityQueue 的泛型 T 会传递到构造函数的 compareFn 参数,保证队列元素类型和比较函数参数类型一致。

关于比较函数的约定:sort 的比较函数要求返回负数、零或正数。如果你的自定义数据结构对返回值有不同的约定(例如只需要 -1 | 0 | 1),可以用字面量联合类型 (a: T, b: T) => -1 | 0 | 1 来标注,但要注意 a - b 的结果类型是 number,需要手动转换。
JavaScript比较参数在TypeScript里类型怎么标
图4:JavaScript 比较参数在 TypeScript 中类型标注的要点总览

实用建议

在日常开发中,大多数比较函数不需要单独提取类型——只有当你发现自己在多处重复写同样的签名时,才值得抽取 CompareFn<T>。内联写法配合 TypeScript 的自动推断已经足够好用,抽取的时机取决于复用频率。

如果项目使用 ESLint 的 @typescript-eslint 规则集,可以开启 @typescript-eslint/no-unsafe-argument 和 @typescript-eslint/no-unsafe-return,这样在比较函数里误用 any 类型时会得到提醒。

最后值得记住的是,TypeScript 的类型标注在编译后会被完全移除,不影响运行时性能。比较函数的类型标注纯粹是为了开发阶段的正确性检查,让编译器帮你在写代码时就发现参数传错、返回值写错的问题。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章