普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月24日首页

高阶函数与泛型函数的类型体操

作者 wuhen_n
2026年1月24日 14:57

高阶函数和泛型是函数式编程的核心,也是 TypeScript 类型系统最强大的部分。掌握这部分内容以后,我们会发现 TypeScript 不仅能检查类型,还能推导类型、组合类型,甚至进行类型运算。

泛型参数约束:给类型参数加"限制条件"

基础泛型约束:确保类型具有某些特性

泛型约束就像给类型参数加上"限制条件",告诉TypeScript:"这个类型参数 T,必须满足某些条件"。这样我们就能在函数体内安全地使用 T 的特定属性。

// 基础约束:T必须具有length属性
interface HasLength {
  length: number;  // 定义了一个接口,要求有length属性
}

// T extends HasLength 表示:T必须有length属性
function logLength<T extends HasLength>(item: T): void {
  // 因为T一定有length,所以可以安全访问
  console.log(`长度: ${item.length}`);
}

// ✅ 正确使用
logLength("hello");       // string有length属性
logLength([1, 2, 3]);     // 数组有length属性
logLength({ length: 5 }); // 对象有length属性

// ❌ 错误使用
// logLength(42);           // number没有length属性 - 编译错误!
// logLength(true);         // boolean没有length属性 - 编译错误!

多个约束:必须同时满足多个条件

interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

// T extends HasId & HasName 表示:T必须同时满足HasId和HasName的要求
function processItem<T extends HasId & HasName>(item: T): string {
  // 这里可以安全访问id和name
  return `${item.id}: ${item.name}`;
}

// ✅ 必须同时有id和name
processItem({ id: 1, name: "zhangsan", age: 25 }); // ✅ 有id和name,age额外属性不影响
// processItem({ id: 1 }); // ❌ 缺少name - 编译错误!
// processItem({ name: "lisi" }); // ❌ 缺少id - 编译错误!

注:约束只在编译时检查,以确保类型安全。

keyof:更精确的约束

keyof T 可以获取类型T的所有键(属性名)的联合类型,比如:keyof {name: string, age: number}就是 name: string | age: number ,这样可以实现完全类型安全的属性访问。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  // K extends keyof T 表示:K必须是T的键之一
  return obj[key];
}

const person = { name: "zhangsan", age: 25, email: "zhangsan@example.com" };

// ✅ 正确:访问存在的属性
getProperty(person, "name");  // 返回string类型
getProperty(person, "age");   // 返回number类型

// ❌ 错误:访问不存在的属性
// getProperty(person, "address"); // 编译错误:address不是person的键

条件类型约束

条件类型类似于三元表达式,只是适用于类型:T extends U ? X : Y 。如果T可以赋值给U,则类型为X,否则为Y。

type ExtractStringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

函数作为参数的类型推导

函数参数的类型推导机制

在 TypeScript 中,当把一个函数作为参数传给另一个函数时,TypeScript 会根据上下文自动推导参数类型。

function mapArray<T, U>(
  array: T[],  // 第一个参数是T类型的数组
  callback: (item: T, index: number) => U  // 第二个参数是回调函数
): U[] {  // 返回U类型的数组
  return array.map(callback);
}

const numbers = [1, 2, 3];
const strings = mapArray(numbers, (n, i) => {
  // n自动推导为number,i自动推导为number
  return `数字${n}在位置${i}`;
});

上述代码中,TypeScript 的推导过程如下:

  1. 看到 numbersnumber[],所以推导出 T = number
  2. 在回调函数中,n 自动推导为 numberi 自动推导为 number
  3. 回调函数的返回值类型是 string,所以推导出 U = string
  4. 因此,整个函数返回值类型是 string[]

TypeScript可以根据上下文自动推导类型,推导过程是双向的:既可以从左到右推导,也可以从右到左推导。具体的泛型参数 TU 会在调用时被具体化。

高阶函数的类型参数推断

本节开始之前,我们先要明白高阶函数是什么?高阶函数本质就是接受 函数 作为 参数,或 返回函数 作为 结果 的函数。

function compose<A, B, C>(
  f: (b: B) => C,      // 接受B返回C的函数
  g: (a: A) => B       // 接受A返回B的函数
): (a: A) => C {       // 返回接受A返回C的函数
  return (a: A) => f(g(a));
}

// 使用:TypeScript自动推导所有类型
const addOne = (x: number) => x + 1;      // number => number
const toString = (x: number) => x.toString(); // number => string

const addOneThenToString = compose(toString, addOne);
// addOneThenToString类型: (x: number) => string

const result = addOneThenToString(5); // "6"

上述代码中,TypeScript 的推导过程如下:

  1. const toString = (x: number) => x.toString(); 接收 number 类型,并返回 string 类型,所以 compose() 函数中 f: (b: B) => CBnumberCstring
  2. const addOne = (x: number) => x + 1; 接收 number 类型,并返回 number 类型,所以 compose() 函数中 g: (a: A) => B AnumberBnumber
  3. 所以最终的推导结果为:A = number, B = number, C = string

柯里化函数的类型定义

什么是柯里化函数

柯里化(Currying)函数 其实就是把接受多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单来说就是把 f(a, b, c) 变成 f(a)(b)(c) 的函数。

type Curried<A, B, R> = (a: A) => (b: B) => R;

function curry<A, B, R>(fn: (a: A, b: B) => R): Currie2<A, B, R> {
  return (a: A) => (b: B) => fn(a, b);
}

const add = (x: number, y: number) => x + y;
const curriedAdd = curry(add);

// 分步调用
const add5 = curriedAdd(5);   // 返回新函数:接受一个数,加上5
const result = add5(3);       // 8

// 也可以连续调用
const result2 = curriedAdd(5)(3); // 8

自动柯里化:处理任意数量参数

在 TypeScript 中,我们可以用递归类型处理任意数量的参数:判断形参 Args 是否可以进行分解,分解为第一个参数 First 和 剩余参数 Rest。如果可以,则继续采用递归的方式,对剩余参数 Rest 进一步分解,直至无法分解,即只剩下一个参数。

type Curried<Args extends any[], R> = 
  Args extends [infer First, ...infer Rest]
    ? (arg: First) => Curried<Rest, R>  // 返回接受First的函数,继续处理Rest
    : R;  // 没有参数了,直接返回结果

我们来看一个简单的代码实例:

function curry<Args extends any[], R>(
  fn: (...args: Args) => R
): Curried<Args, R> {
  return function curried(...args: any[]): any {
    // 如果参数数量够了,就调用原函数
    if (args.length >= fn.length) {
      return fn(...args as any);
    }
    // 否则返回新函数,继续收集参数
    return (...moreArgs: any[]) => curried(...args, ...moreArgs);
  } as any;  // 类型断言,因为递归类型比较复杂
}

// 使用:可以柯里化任意参数的函数
const multiply = (a: number, b: number, c: number) => a * b * c;
const curriedMultiply = curry(multiply);

// 可以分任意步调用
const multiplyBy2 = curriedMultiply(2);      // 返回:接受两个参数的函数
const multiplyBy2And3 = multiplyBy2(3);      // 返回:接受一个参数的函数
const finalResult = multiplyBy2And3(4);      // 24

// 也可以连续调用
const finalResult2 = curriedMultiply(2)(3)(4); // 24

函数组合的类型体操

管道(Pipe)操作

所谓 管道,就是把多个函数连接起来,用前一个函数的输出作为后一个函数的输入。

管道定义

function pipe<F extends [(...args: any[]) => any, ...Array<(arg: any) => any>]>(
  ...functions: F
): (...args: Parameters<F[0]>) => ReturnType<F[F['length'] - 1]> {
  return (...args: Parameters<F[0]>) => {
    let result = functions[0](...args);  // 执行第一个函数
    for (let i = 1; i < functions.length; i++) {
      result = functions[i](result);  // 用前一个结果调用下一个函数
    }
    return result;
  } as any;
}

上述代码中,F是函数数组,第一个函数接受任意参数,后续函数接受前一个函数的返回值。

管道使用

const add = (x: number) => x + 1;
const multiply = (x: number) => x * 2;
const toString = (x: number) => x.toString();

// 创建处理管道
const process = pipe(add, multiply, toString);
const result = process(5);
console.log(result); // "12"

上述代码的输出结果是 "12" ,我们用直观一点的理解就是:process(5) 相当于 toString(multiply(add(5)))

组合(Compose)操作

组合 操作正好与 管道 操作相反,它是从右到左组合函数。

组合定义

function compose<Functions extends [(arg: any) => any, ...Array<(arg: any) => any>]>(
  ...functions: Functions
): (arg: any) => ReturnType<Functions[0]> {
  return (arg: any) => {
    let result = arg;
    // 从右向左执行
    for (let i = functions.length - 1; i >= 0; i--) {
      result = functions[i](result);
    }
    return result;
  } as any;
}

组合使用

const toUpper = (s: string) => s.toUpperCase();
const addExclamation = (s: string) => s + "!";
const repeat = (s: string) => s + s;

// 从右到左组合:repeat → addExclamation → toUpper
const shout = compose(toUpper, addExclamation, repeat);
const shouted = shout("hello"); 
console.log(shouted); // "HELLOHELLO!"

上述代码的输出结果是:"HELLOHELLO!" 其执行过程如下:

  • shout("hello") 函数会先调用 repeat() 函数,输出结果是:"hellohello"
  • 接着再调用 addExclamation() 函数,输出结果是:"hellohello!"
  • 最后再调用 toUpper() 函数,输出结果是:"HELLOHELLO!"

异步函数组合

当函数返回Promise时,我们需要特殊的组合方式。

异步管道的简单实现

async function asyncPipe<Functions extends Array<(arg: any) => any>>(
  ...functions: Functions
): (input: any) => Promise<any> {
  return async (input: any) => {
    let result = await Promise.resolve(input);  // 处理可能是Promise的输入
    for (const fn of functions) {
      result = await Promise.resolve(fn(result));  // 等待每个函数完成
    }
    return result;
  };
}

异步管道的简单实例

const fetchUser = async (id: number) => {
  console.log(`获取用户${id}...`);
  return { id, name: "User" + id };
};

const processUser = (user: any) => {
  console.log("处理用户...");
  return { ...user, processed: true };
};

const saveUser = async (user: any) => {
  console.log("保存用户...");
  return user;
};

// 创建异步管道:fetchUser → processUser → saveUser
const userPipeline = asyncPipe(fetchUser, processUser, saveUser);

// 执行管道
userPipeline(1).then(user => {
  console.log("最终用户:", user);
});

结语

类型体操的目的是让代码更安全、更清晰,而不是炫耀技术。我们应该从实际需求出发,选择最简单的解决方案。

对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

❌
❌