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 项目的代码结构。


参考资源

相关推荐
程序员buddha7 小时前
TypeScript详细教程
javascript·ubuntu·typescript
We་ct8 小时前
LeetCode 50. Pow(x, n):从暴力法到快速幂的优化之路
开发语言·前端·javascript·算法·leetcode·typescript·
SmalBox8 小时前
【节点】[Power节点]原理解析与实际应用
unity3d·游戏开发·图形学
comedate8 小时前
[TypeScript] TypeScript 学习从入门到精通
ubuntu·typescript·前端语言
Wect9 小时前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·算法·typescript
南無忘码至尊9 小时前
Unity学习90天-第2天-认识键盘 / 鼠标输入(PC)并实现WASD 移动,鼠标控制物体转向
学习·unity·c#·游戏开发
拖孩10 小时前
我用 AI 搓了一个"比谁更持久"的微信小游戏,AI实现只用了一天,微信审核却用了一个月!!!
微信小程序·ai编程·游戏开发
橘子编程1 天前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
LcGero1 天前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机