TypeScript 快速上手:泛型与工具类型

1. 泛型概述

泛型将类型作为参数,使函数、类或接口能够处理多种类型而不丢失类型信息。与 C# 泛型概念相似,但 TypeScript 泛型在编译后会被擦除,仅作用于类型检查阶段。

tsx 复制代码
function identity<T>(value: T): T {
    return value;
}

const num = identity<number>(42);      // num: number
const str = identity<string>("text");  // str: string
const inferred = identity(true);       // inferred: boolean(类型推断)

2. 泛型函数

泛型函数可定义类型参数,用于约束参数与返回值之间的关系。

tsx 复制代码
function firstElement<T>(arr: T[]): T | undefined {
    return arr.length > 0 ? arr[0] : undefined;
}

const numbers = [1, 2, 3];
const firstNum = firstElement(numbers);  // firstNum: number | undefined

const strings = ["a", "b"];
const firstStr = firstElement(strings);  // firstStr: string | undefined

2.1 游戏场景:配置表加载函数

tsx 复制代码
interface ConfigRow {
    id: number;
}

function loadTable<T extends ConfigRow>(tableName: string): T[] {
    // 模拟读取 JSON 或解析 CSV 返回数据
    const rawData = fetchTableData(tableName);
    return rawData as T[];
}

interface MonsterRow extends ConfigRow {
    name: string;
    health: number;
    attack: number;
}

interface SkillRow extends ConfigRow {
    name: string;
    manaCost: number;
    cooldown: number;
}

const monsters = loadTable<MonsterRow>("Monster");
const skills = loadTable<SkillRow>("Skill");

// 类型安全:访问属性有完整提示
monsters[0].health;
skills[0].cooldown;

2.2 泛型约束

extends 关键字约束类型参数必须满足特定形状,类似于 C# 的 where T : classwhere T : struct

tsx 复制代码
interface HasId {
    id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

const monster = findById(monsters, 101);  // monster: MonsterRow | undefined

多重约束通过交叉类型实现:

tsx 复制代码
interface Identifiable { id: number; }
interface Nameable { name: string; }

function processEntity<T extends Identifiable & Nameable>(entity: T): void {
    console.log(`Entity ${entity.id}: ${entity.name}`);
}

3. 泛型类

泛型类允许类成员共享类型参数,典型应用包括数据结构与工厂类。

tsx 复制代码
class ObjectPool<T> {
    private pool: T[] = [];
    private createFn: () => T;
    private resetFn: (obj: T) => void;

    constructor(createFn: () => T, resetFn: (obj: T) => void, initialSize: number = 0) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(this.createFn());
        }
    }

    public acquire(): T {
        if (this.pool.length > 0) {
            return this.pool.pop()!;
        }
        return this.createFn();
    }

    public release(obj: T): void {
        this.resetFn(obj);
        this.pool.push(obj);
    }
}

// 使用示例:子弹对象池
interface Bullet {
    active: boolean;
    position: [number, number];
    velocity: [number, number];
}

const bulletPool = new ObjectPool<Bullet>(
    () => ({ active: false, position: [0, 0], velocity: [0, 0] }),
    (b) => { b.active = false; b.position = [0, 0]; b.velocity = [0, 0]; },
    20
);

const bullet = bulletPool.acquire();
bullet.active = true;
// ... 使用后回收
bulletPool.release(bullet);

4. 泛型接口

接口可定义泛型参数,用于描述灵活的数据结构。

tsx 复制代码
interface Repository<T> {
    find(id: number): T | undefined;
    findAll(): T[];
    save(entity: T): void;
    delete(id: number): boolean;
}

class MonsterRepository implements Repository<MonsterRow> {
    private data: Map<number, MonsterRow> = new Map();

    find(id: number): MonsterRow | undefined {
        return this.data.get(id);
    }

    findAll(): MonsterRow[] {
        return Array.from(this.data.values());
    }

    save(entity: MonsterRow): void {
        this.data.set(entity.id, entity);
    }

    delete(id: number): boolean {
        return this.data.delete(id);
    }
}

5. keyof 与索引访问类型

5.1 keyof 类型运算符

keyof T 返回类型 T 所有键名组成的字面量联合类型。

tsx 复制代码
interface PlayerData {
    id: number;
    name: string;
    level: number;
    exp: number;
}

type PlayerKey = keyof PlayerData;  // "id" | "name" | "level" | "exp"

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

const player: PlayerData = { id: 1, name: "Hero", level: 10, exp: 1500 };
const level = getProperty(player, "level");     // level: number
const name = getProperty(player, "name");       // name: string
// const invalid = getProperty(player, "health");  // 错误:health 不是 PlayerData 的属性

游戏场景中的应用:安全的属性修改器。

tsx 复制代码
function modifyStat<T, K extends keyof T>(
    target: T,
    stat: K,
    delta: T[K] extends number ? number : never
): void {
    if (typeof target[stat] === "number") {
        (target[stat] as number) += delta;
    }
}

modifyStat(player, "level", 1);   // 合法
// modifyStat(player, "name", 1);  // 错误:name 非 number 类型

5.2 索引访问类型

T[K] 获取类型 T 中键 K 对应的属性类型。

tsx 复制代码
type PlayerNameType = PlayerData["name"];           // string
type PlayerNumericStats = PlayerData["level" | "exp"];  // number

6. 内置工具类型

TypeScript 提供一组实用类型,基于泛型与映射类型实现,用于常见类型变换操作。

6.1 Partial

将类型 T 的所有属性变为可选。常用于对象更新、Buff 临时修改场景。

tsx 复制代码
interface BuffEffect {
    attackBonus: number;
    defenseBonus: number;
    speedMultiplier: number;
}

function applyBuff(target: PlayerData, buff: Partial<BuffEffect>): void {
    // 仅应用传入的加成,未传入的保持原值
    if (buff.attackBonus) { /* ... */ }
    if (buff.speedMultiplier) { /* ... */ }
}

applyBuff(player, { attackBonus: 15 });  // 仅传入部分属性

6.2 Readonly

将类型 T 的所有属性变为只读。适用于不可变配置、常量数据。

tsx 复制代码
interface GameConfig {
    maxPlayers: number;
    tickRate: number;
    mapSize: number;
}

const config: Readonly<GameConfig> = {
    maxPlayers: 50,
    tickRate: 30,
    mapSize: 1024
};

// config.maxPlayers = 60;  // 错误:只读属性不可修改

6.3 Pick<T, K>

从类型 T 中选取键 K 的子集构造新类型。

tsx 复制代码
interface CharacterFull {
    id: number;
    name: string;
    health: number;
    mana: number;
    position: [number, number];
    inventory: Item[];
}

type NetworkSyncData = Pick<CharacterFull, "id" | "position" | "health">;

function syncState(data: NetworkSyncData): void {
    // 仅同步必要字段,减少带宽
    console.log(`Sync ID ${data.id}: pos=(${data.position}), hp=${data.health}`);
}

6.4 Omit<T, K>

从类型 T 中排除键 K 构造新类型,与 Pick 互补。

tsx 复制代码
type CharacterWithoutInventory = Omit<CharacterFull, "inventory">;

function serializeCharacter(char: CharacterWithoutInventory): string {
    // 序列化时忽略背包数据
    return JSON.stringify(char);
}

6.5 Record<K, V>

构造键类型为 K、值类型为 V 的对象类型。常用于字典、映射表。

tsx 复制代码
type SkillId = 101 | 102 | 103;
type SkillNameMap = Record<SkillId, string>;

const skillNames: SkillNameMap = {
    101: "Slash",
    102: "Dash",
    103: "Fireball"
};

// 更常见的用法:字符串键映射到配置类型
type ConfigDictionary = Record<string, MonsterRow>;
const monsterDict: ConfigDictionary = {};

monsterDict["goblin"] = { id: 1, name: "Goblin", health: 30, attack: 8 };

6.6 Exclude<T, U> 与 Extract<T, U>

从联合类型中排除或提取特定成员。

tsx 复制代码
type AllStatus = "Idle" | "Run" | "Attack" | "Death" | "Stun";
type MovableStatus = Exclude<AllStatus, "Death" | "Stun">;  // "Idle" | "Run" | "Attack"
type UncontrollableStatus = Extract<AllStatus, "Death" | "Stun">;  // "Death" | "Stun"

function canMove(status: AllStatus): status is MovableStatus {
    return status !== "Death" && status !== "Stun";
}

6.7 ReturnType

获取函数类型 T 的返回值类型。

tsx 复制代码
function createEnemy(): { id: number; type: string } {
    return { id: 1, type: "Goblin" };
}

type EnemyInstance = ReturnType<typeof createEnemy>;  // { id: number; type: string; }

6.8 Parameters

获取函数类型 T 的参数类型元组。

tsx 复制代码
function dealDamage(target: Character, amount: number, critical: boolean): void { }

type DamageParams = Parameters<typeof dealDamage>;  // [Character, number, boolean]

7. 自定义工具类型

结合泛型、映射类型与条件类型可创建领域专用的工具类型。

7.1 可空类型

tsx 复制代码
type Nullable<T> = { [P in keyof T]: T[P] | null };

interface SaveData {
    lastCheckpoint: string;
    playTime: number;
}

type NullableSaveData = Nullable<SaveData>;
// { lastCheckpoint: string | null; playTime: number | null; }

7.2 深度只读

tsx 复制代码
type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface NestedConfig {
    graphics: {
        resolution: [number, number];
        quality: number;
    };
    audio: {
        volume: number;
    };
}

const settings: DeepReadonly<NestedConfig> = {
    graphics: { resolution: [1920, 1080], quality: 5 },
    audio: { volume: 0.8 }
};

// settings.graphics.quality = 3;  // 错误:深层属性亦为只读

8. 条件类型简介

条件类型根据类型关系选择分支,语法为 T extends U ? X : Y

tsx 复制代码
type IsNumber<T> = T extends number ? true : false;
type A = IsNumber<string>;  // false
type B = IsNumber<42>;      // true

// 提取函数返回值为 Promise 的实际类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Result1 = UnwrapPromise<Promise<number>>;  // number
type Result2 = UnwrapPromise<string>;           // string

infer 关键字在条件类型分支中声明待推断的类型变量,用于提取类型信息。

8.1 游戏场景:技能效果类型推断

tsx 复制代码
interface DamageEffect { type: "damage"; value: number; }
interface HealEffect { type: "heal"; value: number; }
interface BuffEffect { type: "buff"; buffId: number; duration: number; }

type SkillEffect = DamageEffect | HealEffect | BuffEffect;

// 根据 type 字段提取对应效果类型
type EffectByType<T extends SkillEffect["type"]> = Extract<SkillEffect, { type: T }>;

type DamageOnly = EffectByType<"damage">;   // DamageEffect
type HealOnly = EffectByType<"heal">;       // HealEffect

9. 游戏场景综合示例

9.1 泛型事件系统

tsx 复制代码
type EventMap = {
    "PlayerDied": { playerId: number; killerId?: number };
    "ItemPickedUp": { itemId: number; quantity: number };
    "LevelUp": { playerId: number; newLevel: number };
};

class TypedEventBus {
    private listeners: Map<keyof EventMap, Function[]> = new Map();

    public on<K extends keyof EventMap>(
        event: K,
        handler: (data: EventMap[K]) => void
    ): void {
        const handlers = this.listeners.get(event) || [];
        handlers.push(handler);
        this.listeners.set(event, handlers);
    }

    public emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
        const handlers = this.listeners.get(event) || [];
        handlers.forEach(h => h(data));
    }
}

const bus = new TypedEventBus();
bus.on("PlayerDied", (data) => {
    console.log(`Player ${data.playerId} died, killer: ${data.killerId ?? "unknown"}`);
    // data 自动推断为 { playerId: number; killerId?: number }
});
bus.emit("PlayerDied", { playerId: 1, killerId: 5 });

9.2 泛型状态机

tsx 复制代码
class StateMachine<TState extends string, TContext> {
    private currentState: TState;
    private transitions: Map<TState, Partial<Record<TState, (ctx: TContext) => boolean>>> = new Map();

    constructor(initialState: TState) {
        this.currentState = initialState;
    }

    public addTransition(
        from: TState,
        to: TState,
        condition?: (ctx: TContext) => boolean
    ): void {
        if (!this.transitions.has(from)) {
            this.transitions.set(from, {});
        }
        this.transitions.get(from)![to] = condition || (() => true);
    }

    public tryTransition(to: TState, context: TContext): boolean {
        const fromTransitions = this.transitions.get(this.currentState);
        if (!fromTransitions) return false;

        const condition = fromTransitions[to];
        if (condition && condition(context)) {
            this.currentState = to;
            return true;
        }
        return false;
    }

    public get state(): TState {
        return this.currentState;
    }
}

// 使用示例:敌人 AI 状态机
type EnemyState = "Idle" | "Patrol" | "Chase" | "Attack" | "Return";
interface EnemyContext {
    targetInSight: boolean;
    distanceToTarget: number;
    health: number;
}

const ai = new StateMachine<EnemyState, EnemyContext>("Idle");
ai.addTransition("Idle", "Patrol");
ai.addTransition("Patrol", "Chase", ctx => ctx.targetInSight);
ai.addTransition("Chase", "Attack", ctx => ctx.distanceToTarget < 5);
ai.addTransition("Attack", "Chase", ctx => ctx.distanceToTarget >= 5);
ai.addTransition("Chase", "Return", ctx => !ctx.targetInSight);
ai.addTransition("Return", "Idle", ctx => ctx.distanceToTarget > 20);

10. 本篇小结

  • 泛型提供类型参数化能力,在函数、类、接口中实现类型安全的复用逻辑。
  • extends 约束类型参数,keyof 获取对象键名联合类型。
  • 内置工具类型(PartialReadonlyPickOmitRecord 等)覆盖常见类型变换需求。
  • 条件类型与 infer 实现高级类型推断,适用于复杂场景。
  • 游戏开发中泛型广泛应用于配置加载、对象池、事件系统与状态机,确保编译期类型正确性。

下一篇将讨论模块系统与命名空间,介绍如何组织大型 TypeScript 项目的代码结构。


参考资源

相关推荐
We་ct20 小时前
LeetCode 5. 最长回文子串:DP + 中心扩展
前端·javascript·算法·leetcode·typescript
Wect1 天前
LeetCode 97. 交错字符串:动态规划详解
前端·算法·typescript
漫游的渔夫2 天前
前端开发者做多步 Agent:别让 AI 边想边乱跑,用 Plan-Act-Observe 稳住 4 步任务
前端·人工智能·typescript
Elastic 中国社区官方博客2 天前
用于 JavaScript 和 TypeScript 的 ES|QL 查询构建器:流式、类型安全的查询构建
大数据·javascript·数据库·elasticsearch·搜索引擎·typescript·全文检索
小爬的老粉丝2 天前
把 Office 预览搬进浏览器:一次仍在继续的纯前端长跑
前端·typescript·docx·ppt·doc·pptx·office预览
SmalBox2 天前
【节点】[Remap节点]原理解析与实际应用
unity3d·游戏开发·图形学
Wect2 天前
LeetCode 5. 最长回文子串:DP + 中心扩展
前端·算法·typescript
漫游的渔夫2 天前
前端开发者做 Agent:别写成一次请求,用 5 步受控循环防止 AI 乱跑
前端·人工智能·typescript
垦利不3 天前
TS基础篇
开发语言·前端·typescript
涵涵(互关)3 天前
GoView各项目文件中的相关语法3
前端·vue.js·typescript