Typescript中的泛型

前言

泛型是 TypeScript 中最为强大的特性之一,它为我们提供了创建可重用、类型安全的代码组件的能力。本文将全面地介绍 TypeScript 泛型的各个方面。

一、泛型的基本概念

1.1 什么是泛型?

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。这种特性可以极大地增加代码的灵活性和可重用性。

// 不使用泛型的例子 - 只能返回数字类型
function identityNumber(arg: number): number {
    return arg;
}

// 使用泛型的例子 - 可以返回任何类型
function identity<T>(arg: T): T {
    return arg;
}

// 使用示例
let output1 = identity<string>("myString");  // 类型为 string
let output2 = identity<number>(100);       // 类型为 number

1.2 为什么需要泛型?

在没有泛型的情况下,开发者通常面临两种选择:

  1. 为每种类型编写重复代码
  2. 使用any类型(失去类型安全性)

泛型完美解决了这一困境,实现了"一次编写,多类型使用"的目标,同时保持类型安全。

二、泛型的基本用法

2.1 泛型函数

泛型函数是最基本的泛型应用形式:

function logAndReturn<T>(value: T): T {
    console.log(value);
    return value;
}

// 使用
const stringResult = logAndReturn<string>("Hello");
const numberResult = logAndReturn<number>(42);

TypeScript 的类型推断机制使得我们通常可以省略显式的类型参数:

const inferredString = logAndReturn("Hello");  // 推断为string
const inferredNumber = logAndReturn(42);      // 推断为number

2.2 泛型接口

泛型接口允许我们定义可以适应多种类型的接口:

interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

// 使用
let pair1: KeyValuePair<number, string> = { key: 1, value: "Apple" };
let pair2: KeyValuePair<string, boolean> = { key: "isAvailable", value: true };

2.3 泛型类

泛型类使得类可以处理多种数据类型:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
    
    constructor(zeroValue: T, add: (x: T, y: T) => T) {
        this.zeroValue = zeroValue;
        this.add = add;
    }
}

// 使用
let myNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myNumber.add(5, 10));  // 15

let myString = new GenericNumber<string>("", (x, y) => x + y);
console.log(myString.add("Hello", " World"));  // "Hello World"

2.4 泛型标识符

2.4.1 通用类型标识符

T (Type) - 最通用的类型参数
// 基本泛型函数
function identity<T>(arg: T): T {
    return arg;
}

const result = identity<string>("Hello");  // result 类型为 string
const inferred = identity(42);            // inferred 类型为 number

2.4.2 集合类标识符

E (Element) - 集合元素类型
// 数组处理函数
function firstElement<E>(arr: E[]): E | undefined {
    return arr[0];
}

const num = firstElement([1, 2, 3]);      // num 类型为 number | undefined
const str = firstElement(["a", "b"]);     // str 类型为 string | undefined

2.4.3. 键值相关标识符

K(Key) - 对象键类型
// 获取对象所有键的类型
function getKeys<K extends string>(obj: Record<K, any>): K[] {
    return Object.keys(obj) as K[];
}

const keys = getKeys({ name: "Alice", age: 30 });  // keys 类型为 ("name" | "age")[]
V(Value) - 对象值类型
// 反转键值对
function invert<K extends string, V extends string>(obj: Record<K, V>): Record<V, K> {
    return Object.fromEntries(
        Object.entries(obj).map(([k, v]) => [v, k])
    ) as Record<V, K>;
}

const inverted = invert({ a: "x", b: "y" });  // 类型为 { x: "a", y: "b" }

2.4.4 函数相关标识符

R (Return) - 函数返回类型
// 包装函数返回值
function wrapResult<R>(fn: () => R): { result: R } {
    return { result: fn() };
}

const wrapped = wrapResult(() => "Hello");  // wrapped 类型为 { result: string }
A (Argument)- 函数参数类型
// 函数参数记录器
function logArgument<A>(arg: A): A {
    console.log("Argument:", arg);
    return arg;
}

const logged = logArgument(123);  // logged 类型为 number
U - 第二个通用类型参数
// 合并两个不同类型的值
function mergeValues<T, U>(first: T, second: U): T & U {
    return { ...first, ...second };
}

const merged = mergeValues(
    { name: "Alice" }, 
    { age: 30 }
);  // 类型为 { name: string } & { age: number }

2.4.5 描述性标识符示例

TInputTOutput - 输入输出类型
// 数据转换函数
function transform<TInput, TOutput>(
    input: TInput,
    converter: (input: TInput) => TOutput
): TOutput {
    return converter(input);
}

const output = transform("123", (s) => parseInt(s));  // output 类型为 number

用法总结

  1. 简单场景:优先使用单字母标识符 (T, K, V 等)
  2. 复杂场景:使用描述性标识符提高可读性 (TResult, TInput 等)
  3. 多参数:按顺序使用 T, U, V 或更具描述性的名称
  4. 一致性:在项目中保持命名风格一致
  5. 文档注释:为复杂泛型添加注释说明类型参数的用途
标识符典型使用场景示例
T基础类型(Type)Array<T>
K键类型(Key)Record<K,V>
V值类型(Value)Map<K,V>
E元素/事件类型(Element)Event<E>
R返回类型(Return)Promise<R>

特殊场景处理

  • 多个泛型参数时建议保持字母顺序关系:

    function convert<T, U extends T>(input: T): U {...}
    
  • 嵌套泛型使用层级命名:

    type Nested<T, U extends keyof T> = {
      [K in U]: T[K][];
    }
    

三、泛型约束

3.1 基本约束

有时我们需要限制泛型的类型范围,这时可以使用泛型约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // 现在我们知道 arg 有 .length 属性
    return arg;
}

// 使用
loggingIdentity("hello");  // OK
loggingIdentity([1, 2, 3]); // OK
loggingIdentity(3);  // Error: number 没有 .length 属性

3.2 多重约束

泛型参数可以同时继承多个类型:

interface Printable {
    print(): void;
}

interface Loggable {
    log(): void;
}

function processItem<T extends Printable & Loggable>(item: T): void {
    item.print();
    item.log();
}

3.3 使用类型参数约束

我们可以使用keyof关键字来约束类型参数必须是另一个类型的属性名:

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

let x = { a: 1, b: 2, c: 3 };

getProperty(x, "a"); // OK
getProperty(x, "d"); // Error: 参数类型 '"d"' 不能赋值给参数类型 '"a" | "b" | "c"'

四、高级泛型特性

4.1 泛型默认类型

泛型默认类型的语法类似于函数参数的默认值:

基本语法

<T = DefaultType>

泛型函数中的默认类型

function createArray<T = string>(length: number, value: T): T[] {
    return Array(length).fill(value);
}

// 使用默认类型
const strArray = createArray(3, "x");  // T 推断为 string
console.log(strArray); // ["x", "x", "x"]

// 显式指定类型
const numArray = createArray<number>(3, 5);
console.log(numArray); // [5, 5, 5]

泛型接口中的默认类型

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

// 使用默认类型
const pair1: KeyValuePair = { key: "age", value: 30 };

// 覆盖部分默认类型
const pair2: KeyValuePair<number> = { key: 1, value: 100 };

// 完全覆盖默认类型
const pair3: KeyValuePair<boolean, string> = { key: true, value: "yes" };

泛型类中的默认类型

class Container<T = any> {
    constructor(private content: T) {}
    
    getContent(): T {
        return this.content;
    }
}

// 使用默认类型any
const anyContainer = new Container("可以是任何类型");
console.log(anyContainer.getContent());

// 指定具体类型
const stringContainer = new Container<string>("必须是字符串");
console.log(stringContainer.getContent());

4.2 条件类型

条件类型允许我们根据条件选择类型:

基本语法

T extends U ? X : Y

这个类型表达式表示:如果类型 T 可以赋值给类型 U,则结果类型为 X,否则为 Y。

示例

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"

4.3 映射类型

映射类型是 TypeScript 中强大的泛型特性,它允许我们基于现有类型创建新类型,通过转换现有类型的属性来生成新的类型结构。映射类型极大地增强了 TypeScript 的类型系统表达能力,让我们能够以编程方式操作和转换类型。
(映射类型允许我们基于旧类型创建新类型)

基本语法

{ [P in K]: T }

其中:

  • K 是一个可迭代的类型(通常是 keyof T)
  • P 是每次迭代中当前的属性名
  • T 是属性的类型(可以基于 P 进行计算)

示例

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 使用
interface Person {
    name: string;
    age: number;
}

type ReadonlyPerson = Readonly<Person>;
type PartialPerson = Partial<Person>;

五、泛型用法总结

  1. 命名约定

    • 通常使用单个大写字母作为类型变量名(如 T、K、V 等)
    • 有明确含义时可以使用描述性名称(如 TKey、TValue)
  2. 适度使用

    • 不是所有情况都需要泛型,只在需要灵活性时使用
    • 避免过度复杂的泛型类型
  3. 明确约束

    • 尽可能使用约束来限制类型参数的范围
    • 使用extends关键字明确类型要求
  4. 文档注释

    • 为复杂的泛型类型添加详细的文档注释
    • 使用@typeparam标注泛型参数
/**
 * 将两个对象合并
 * @typeparam T - 第一个对象的类型
 * @typeparam U - 第二个对象的类型
 * @param obj1 - 第一个对象
 * @param obj2 - 第二个对象
 * @returns 合并后的对象
 */
function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

六、总结

TypeScript 泛型是一个强大的工具,它可以帮助我们:

  1. 编写更灵活、可重用的代码
  2. 保持类型安全,减少运行时错误
  3. 创建更抽象的组件和数据结构
  4. 提高代码的可读性和可维护性

从简单的泛型函数到复杂的条件类型和映射类型,泛型为 TypeScript 的类型系统提供了极大的表现力。通过合理应用泛型,我们可以构建出既灵活又类型安全的代码库,大大提高开发效率和代码质量。

掌握泛型需要实践和耐心,但一旦掌握,它将极大地提升你的 TypeScript 编程能力。建议从简单的泛型函数开始,逐步尝试更复杂的泛型应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值