泛型

泛型(Generics)是TypeScript中一个非常强大的特性,它允许我们在定义函数、接口或类时不预先指定具体的类型,而是在使用时再指定类型。泛型的核心思想是类型参数化,通过泛型我们可以编写出更加灵活、可复用的代码,同时又不失类型安全性。

为什么需要泛型

假设我们需要编写一个函数,它接收一个参数并原样返回,如果不使用泛型,我们可能会这样写。

function identity(arg: any): any {
  return arg;
}

这种写法虽然能够接收任意类型的参数,但返回值类型也变成了any,我们丢失了类型信息。比如传入一个number,TypeScript无法推断返回值也是number类型。

使用泛型则可以完美解决这个问题。

function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42);      // num的类型是number
const str = identity<string>("hello"); // str的类型是string

代码中,T是一个类型参数,它在函数调用时被具体的类型替换,这样既保持了灵活性,又保留了完整的类型信息。

泛型函数

泛型函数是最常见的泛型用法,我们在函数名后使用尖括号<>定义类型参数,下面是一个有趣但有点复杂的例子。

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

const result = swap<string, number>(["hello", 42]); // 类型为[number, string]

例子中,我们定义了两个类型参数TU,函数接收一个元组并交换其中元素的顺序。

在大多数情况下,TypeScript能够根据传入的参数自动推断类型参数,因此我们可以省略显式的类型指定。

const result = swap(["hello", 42]); // TypeScript自动推断类型

此外箭头函数同样支持泛型语法。

const identity = <T>(arg: T): T => {
  return arg;
};

泛型接口

接口也可以使用泛型,这在定义通用数据结构时非常有用。

interface Response<T> {
  code: number;
  message: string;
  data: T;
}

interface User {
  id: number;
  name: string;
}

const userResponse: Response<User> = {
  code: 200,
  message: "success",
  data: {
    id: 1,
    name: "Tom",
  },
};

上面例子中,我们定义了一个通用的响应接口Response<T>,其中data字段的类型由泛型参数T决定,这种模式在处理API响应时非常常见。

泛型接口也可以用于描述函数类型。

interface GenericFunction<T> {
  (arg: T): T;
}

const myIdentity: GenericFunction<number> = (arg) => arg;

泛型类

类同样支持泛型,泛型类在实例化时指定具体类型。

class Container<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}

const numberContainer = new Container<number>(42);
const stringContainer = new Container<string>("hello");

console.log(numberContainer.getValue()); // 42
console.log(stringContainer.getValue()); // hello

注意:泛型类的类型参数只能用于实例成员,不能用于静态成员。这是因为类型参数的作用域和生命周期与实例相关,而静态成员属于类本身,与任何具体实例无关,在Java、C#等语言中也是类似的。

class Container<T> {
  static defaultValue: T; // 错误:静态成员不能引用类型参数
}

泛型约束

有时我们需要限制泛型参数的类型范围,这时可以使用泛型约束。通过extends关键字,我们可以要求类型参数必须满足某些条件。

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
  console.log(arg.length);
}

logLength("hello");     // 正确,字符串有length属性
logLength([1, 2, 3]);   // 正确,数组有length属性
logLength({ length: 10 }); // 正确,对象有length属性
logLength(42);          // 错误,number类型没有length属性

上面例子中,我们约束类型参数T必须具有length属性,这样在函数内部就可以安全地访问arg.length

我们还可以使用类型参数作为另一个类型参数的约束。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Tom", age: 25 };
const name = getProperty(person, "name"); // 正确
const value = getProperty(person, "email"); // 错误,"email"不是person的键

这里K extends keyof T表示K必须是T的键之一,这保证了我们只能访问对象上实际存在的属性。

泛型默认值

类似于函数参数的默认值,泛型参数也可以指定默认类型。

interface Container<T = string> {
  value: T;
}

const c1: Container = { value: "hello" };       // T默认为string
const c2: Container<number> = { value: 42 };    // 显式指定T为number

当泛型有多个类型参数时,有默认值的参数必须放在没有默认值的参数后面。

interface KeyValue<K, V = string> {
  key: K;
  value: V;
}

高级泛型工具类型

TypeScript内置了一些常用的高级泛型工具类型,它们可以帮助我们进行类型转换。

Partial\<T>:将类型T的所有属性变为可选。

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; }

Required\<T>:将类型T的所有属性变为必选。

interface Config {
  host?: string;
  port?: number;
}

type RequiredConfig = Required<Config>;
// 等价于 { host: string; port: number; }

Readonly\<T>:将类型T的所有属性变为只读。

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;
const p: ReadonlyPoint = { x: 1, y: 2 };
p.x = 3; // 错误,属性是只读的

Pick\<T, K>:从类型T中选取部分属性K。

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserBasicInfo = Pick<User, "id" | "name">;
// 等价于 { id: number; name: string; }

Omit\<T, K>:从类型T中排除部分属性K。

type UserWithoutEmail = Omit<User, "email">;
// 等价于 { id: number; name: string; age: number; }

Record\<K, T>:创建一个键类型为K、值类型为T的对象类型。

type StringMap = Record<string, number>;
const scores: StringMap = {
  math: 90,
  english: 85,
};

ReturnType\<T>:获取函数类型T的返回值类型。

function createUser() {
  return { id: 1, name: "Tom" };
}

type User = ReturnType<typeof createUser>;
// 等价于 { id: number; name: string; }

这些高级工具类型在实际开发中使用频率很高,熟练掌握它们可以显著提升代码的类型表达能力。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。