泛型:TypeScript 的类型编程艺术

112 阅读6分钟

泛型:TypeScript 的类型编程艺术

前言:当类型注释不再够用

在 TypeScript 的世界里,大多数情况下我们只是在做"类型注释"——告诉编译器某个变量是什么类型。但当遇到复杂场景时,简单的类型注释就像用记事本写代码一样无力。这时候,泛型登场了。

场景一:函数柯里化的类型魔法

问题:没有泛型的类型黑洞

// 尝试写一个柯里化函数,结果...
function curry(fn: any): any {
  return function curried(...args: any[]) {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return function(...moreArgs: any[]) {
        return curried(...args, ...moreArgs);
      };
    }
  };
}

const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);

// 灾难:完全丢失类型安全!
const step1 = curriedAdd(1);     // any
const result = curriedAdd(1)(2); // any
curriedAdd(1)("hello");          // 运行时才报错!

解决方案:泛型类型编程

// 泛型实现的类型安全柯里化
type Curried<T extends (...args: any[]) => any> = 
  T extends (...args: infer Args) => infer Return
    ? Args extends [infer First, ...infer Rest]
      ? Rest extends []
        ? (arg: First) => Return
        : (arg: First) => Curried<(...args: Rest) => Return>
      : () => Return
    : never;

function curry<T extends (...args: any[]) => any>(fn: T): Curried<T> {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return function(...moreArgs: any[]) {
        return curried(...args, ...moreArgs);
      };
    }
  };
}

// 使用体验:完美类型推断
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add);

// ✅ 每一步都有精确类型
const step1 = curriedAdd(1);        // (b: number) => (c: number) => number
const step2 = curriedAdd(1)(2);     // (c: number) => number
const result = curriedAdd(1)(2)(3); // number

// ✅ 多种调用方式都类型安全
const result1 = curriedAdd(1, 2)(3); // number
const result2 = curriedAdd(1, 2, 3); // number

// ❌ 错误在编写时立即发现
curriedAdd(1)("2"); // 编译错误:类型不匹配

核心价值:泛型让我们能够表达"根据输入函数的类型,动态推导出柯里化后函数的类型"这种复杂逻辑。

场景二:深度 Partial 的递归力量

问题:内置工具的局限性

interface User {
  id: number;
  profile: {
    name: string;
    address: {
      city: string;
      street: string;
    };
  };
}

// 内置 Partial 只能处理第一层
type ShallowPartial = Partial<User>;

const updateData: ShallowPartial = {
  profile: {
    name: "新名字",
    address: { // ❌ 这里 address 还是必须的!
      city: "北京",
      street: "必须填写" // 必须传 street!
    }
  }
};

解决方案:递归泛型类型

// 深度 Partial:不用泛型根本写不出来
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type PartialUser = DeepPartial<User>;

// 现在可以优雅地部分更新了!
const updateData: PartialUser = {
  profile: {
    name: "新名字", 
    address: {
      city: "北京"
      // street 可以不传!
    }
  }
  // id 也可以不传!
};

// 支持无限层级嵌套
const minimalUpdate: PartialUser = {
  profile: {
    address: {} // 只更新到 address 层级
  }
};

核心价值:泛型让我们能够定义递归的类型变换规则,这是普通类型注解无法做到的。

场景三:函数参数的类型提取

问题:手动维护的类型同步

function createUser(name: string, age: number, email: string): User {
  // ...
}

// 手动提取参数类型(容易忘记更新)
type CreateUserParams = [string, number, string];

// 当函数签名改变时...
function createUser(name: string, age: number, email: string, role: string): User {
  // ...
}

// 必须记得手动更新类型(但经常会忘!)
type CreateUserParams = [string, number, string, string]; // 容易漏改

解决方案:泛型类型提取

// 一次定义,自动同步
type FunctionParams<T extends (...args: any[]) => any> = 
  T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number, email: string): User {
  // ...
}

// 自动提取参数类型
type CreateUserParams = FunctionParams<typeof createUser>; // [string, number, string]

// 当函数签名改变时...
function createUser(name: string, age: number, email: string, role: string): User {
  // ...
}

// 类型自动更新!
type CreateUserParams = FunctionParams<typeof createUser>; // [string, number, string, string]

// 实际应用:类型安全的高阶函数
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
  return function(...args: FunctionParams<T>): ReturnType<T> {
    console.log('调用参数:', args);
    return fn(...args);
  } as T;
}

const loggedCreateUser = withLogging(createUser);

// ✅ 完全类型安全!
loggedCreateUser("张三", 25, "zhang@example.com", "admin"); // 正确
loggedCreateUser("李四", "30", "li@example.com"); // 错误:参数类型不匹配

核心价值:泛型让我们能够从现有类型中提取和操作类型信息,实现类型的自动同步。

场景四:条件类型分发

问题:类型逻辑的丢失

function processValue(input: string | number | boolean) {
  if (typeof input === 'string') {
    return input.length; // number
  } else if (typeof input === 'number') {
    return input.toString(); // string
  } else {
    return !input; // boolean
  }
}

// 返回类型怎么写?只能丢失信息
function processValue(input: string | number | boolean): any {
  // ...
}

// 使用时的痛苦
const result1 = processValue("hello"); // any,不知道是 number
const result2 = processValue(42);      // any,不知道是 string

解决方案:条件类型分发

// 泛型条件类型:精确的类型映射
type ProcessResult<T> = 
  T extends string ? number :
  T extends number ? string :
  T extends boolean ? boolean :
  never;

function processValue<T extends string | number | boolean>(input: T): ProcessResult<T> {
  if (typeof input === 'string') {
    return input.length as ProcessResult<T>;
  } else if (typeof input === 'number') {
    return input.toString() as ProcessResult<T>;
  } else {
    return !input as ProcessResult<T>;
  }
}

// 使用:完美的类型推断!
const result1 = processValue("hello"); // number
const result2 = processValue(42);      // string
const result3 = processValue(true);    // boolean

// 联合类型的自动分发
const inputs = ["hello", 42, true] as const;
const results = inputs.map(x => processValue(x));
// 类型: (number | string | boolean)[]
// 知道第一个是 number,第二个是 string,第三个是 boolean

核心价值:泛型让我们能够根据输入类型动态决定输出类型,实现真正的类型级编程。

场景五:类型安全的 Promise 链

问题:Promise 链中的类型丢失

async function fetchUser(id: string): Promise<User> { /* ... */ }
async function validateUser(user: User): Promise<boolean> { /* ... */ }
async function sendEmail(user: User): Promise<void> { /* ... */ }

// 传统的 Promise 链:类型信息逐渐模糊
fetchUser("123")
  .then(user => validateUser(user))    // Promise<boolean>
  .then(isValid => {
    if (isValid) {
      return fetchUser("123").then(user => sendEmail(user)); // Promise<any>
    }
    return false; // 这里类型已经混乱了
  })
  .then(result => {
    // result 是什么类型?谁知道呢!
  });

解决方案:泛型 Promise 流水线

// 类型安全的 Promise 流水线
type PromisePipe<T> = {
  then: <U>(fn: (value: T) => U | Promise<U>) => PromisePipe<U>;
  result: () => Promise<T>;
};

function createPromisePipe<T>(promise: Promise<T>): PromisePipe<T> {
  return {
    then<U>(fn: (value: T) => U | Promise<U>): PromisePipe<U> {
      const newPromise = promise.then(fn);
      return createPromisePipe(newPromise);
    },
    result: () => promise
  };
}

// 使用:完美的类型流转
const result = await createPromisePipe(fetchUser("123"))
  .then(user => validateUser(user))     // 知道这里返回 boolean
  .then(isValid => {
    if (isValid) {
      return fetchUser("123")
        .then(user => sendEmail(user))  // 知道这里返回 void
        .then(() => "邮件发送成功");
    }
    return "验证失败";
  })                                    // 知道这里返回 string
  .result();

// result 的类型是 string,TypeScript 完全知道!

总结:泛型的本质是类型编程

普通类型注释就像给变量贴标签:

const name: string = "张三";
const age: number = 25;

泛型类型编程就像编写操作类型的函数:

// 这是一个"类型函数":输入一个类型 T,输出一个深度可选的类型
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

什么时候必须用泛型?

当你有以下需求时,泛型是唯一选择:

  1. 类型级计算:需要对类型进行递归、条件判断等操作
  2. 类型关系保持:需要维护输入类型与输出类型之间的关系
  3. 动态类型推断:需要根据使用时的具体类型动态确定其他类型
  4. 类型信息提取:需要从现有类型结构中提取部分信息

开始你的类型编程之旅

从最简单的泛型工具开始:

// 工具1:让所有属性变为只读
type ReadonlyDeep<T> = {
  readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];
};

// 工具2:从联合类型中提取函数类型
type FunctionMembers<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never;
}[keyof T];

// 工具3:获取 Promise 的包裹类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

记住:当你发现自己在重复写相似的类型定义,或者需要表达复杂的类型关系时,就是泛型登场的最佳时机!

泛型不是让代码变复杂的元凶,而是让你从重复的类型劳动中解放的利器。开始尝试类型编程,你会发现 TypeScript 的真正威力!