TypeScript 中的 Record:从重构工厂函数说起

作为一名 TypeScript 开发者,我们经常会遇到需要根据字符串键创建不同类型对象的场景。今天,我们就从一个实际的重构案例出发,深入探讨 TypeScript 中 Record 类型的强大功能。

引子:一个冗长的工厂函数

假设我们正在开发一个游戏编辑器,需要根据行为名称创建对应的编辑器实例。最初的代码可能是这样的:

TypeScript 复制代码
private _createDriveBehaviorByName(behaviorName: string): DriveBehaviorEditor | null {
    let behavior: DriveBehaviorEditor | null = null;

    switch (behaviorName) {
        case "DriveLerpPositionEditor":
            behavior = new DriveLerpPositionEditor();
            break;
        case "DriveLerpRotationEditor":
            behavior = new DriveLerpRotationEditor();
            break;
        case "DriveLerpScalingEditor":
            behavior = new DriveLerpScalingEditor();
            break;
        case "DriveLerpVisibleEditor":
            behavior = new DriveLerpVisibleEditor();
            break;
        case "DriveAxisRotateEditor":
            behavior = new DriveAxisRotateEditor();
            break;
        default:
            break;
    }
    
    return behavior;
}

这段代码在严格模式下存在几个问题:

  1. 重复性高 :每个 case 都是类似的 new 操作

  2. 可维护性差:新增行为需要修改 switch 语句

  3. 类型安全性弱behaviorName 是宽泛的 string 类型

  4. 不符合开闭原则:对扩展开放但对修改封闭

让我们看看如何用 Record 类型优雅地解决这个问题。

Record 类型基础

在严格模式下,Record 的类型定义如下:

TypeScript 复制代码
type Record<K extends string | number | symbol, T> = {
    [P in K]: T;
};

它接收两个泛型参数:

  • K:键的类型,必须是 stringnumbersymbol 的子集

  • T:值的类型

核心特点 :在严格模式中,访问 Record 中不存在的键会返回 undefined,这迫使我们必须处理这种情况。

重构第一步:建立构造函数映射

在严格模式下,我们首先需要明确构造函数的类型:

TypeScript 复制代码
// 定义构造函数类型(严格模式要求明确的实例类型)
type DriveBehaviorEditorConstructor = new () => DriveBehaviorEditor;

// 创建映射关系(使用私有静态属性,符合封装原则)
private static readonly behaviorMap: Record<string, DriveBehaviorEditorConstructor> = {
    "DriveLerpPositionEditor": DriveLerpPositionEditor,
    "DriveLerpRotationEditor": DriveLerpRotationEditor,
    "DriveLerpScalingEditor": DriveLerpScalingEditor,
    "DriveLerpVisibleEditor": DriveLerpVisibleEditor,
    "DriveAxisRotateEditor": DriveAxisRotateEditor
};

// 重构后的工厂方法(严格模式要求处理 undefined)
private _createDriveBehaviorByName(behaviorName: string): DriveBehaviorEditor | null {
    const Constructor = YourClass.behaviorMap[behaviorName];
    // 严格模式下必须明确检查 undefined
    return Constructor !== undefined ? new Constructor() : null;
}

严格模式的关键点 :我们显式检查了 Constructor !== undefined,而不是简单的 if (Constructor),这避免了潜在的类型陷阱。

进阶用法:精确类型约束

在严格模式下,使用宽泛的 string 作为键类型会失去很多类型检查能力。让我们引入联合类型:

TypeScript 复制代码
// 使用字面量联合类型(严格模式下推荐)
type BehaviorName = 
    | "DriveLerpPositionEditor"
    | "DriveLerpRotationEditor"
    | "DriveLerpScalingEditor"
    | "DriveLerpVisibleEditor"
    | "DriveAxisRotateEditor";

// 现在 Record 的键是精确类型
private static readonly behaviorMap: Record<BehaviorName, DriveBehaviorEditorConstructor> = {
    "DriveLerpPositionEditor": DriveLerpPositionEditor,
    "DriveLerpRotationEditor": DriveLerpRotationEditor,
    "DriveLerpScalingEditor": DriveLerpScalingEditor,
    "DriveLerpVisibleEditor": DriveLerpVisibleEditor,
    "DriveAxisRotateEditor": DriveAxisRotateEditor
    // 严格模式会检查:是否遗漏了任何 BehaviorName 类型
};

private _createDriveBehaviorByName(behaviorName: BehaviorName): DriveBehaviorEditor {
    const Constructor = YourClass.behaviorMap[behaviorName];
    // 严格模式下,由于 behaviorName 是精确的 BehaviorName,
    // 我们可以确信 Constructor 一定存在
    return new Constructor();
}

严格模式的优势 :TypeScript 会强制要求 behaviorMap 包含联合类型中的每一个键,避免遗漏。

处理动态键:Partial 的应用

在实际项目中,我们可能无法预先确定所有可能的键。在严格模式下,Partial<Record> 是处理这种情况的利器:

TypeScript 复制代码
// 映射可能不完整(某些键可能没有对应的构造函数)
private static readonly optionalBehaviorMap: Partial<Record<string, DriveBehaviorEditorConstructor>> = {
    "DriveLerpPositionEditor": DriveLerpPositionEditor,
    "DriveLerpRotationEditor": DriveLerpRotationEditor
    // 注意:可以只定义部分映射
};

private _createBehaviorSafely(behaviorName: string): DriveBehaviorEditor | null {
    const Constructor = YourClass.optionalBehaviorMap[behaviorName];
    
    // 严格模式要求我们必须处理 undefined
    if (Constructor === undefined) {
        console.warn(`Unknown behavior: ${behaviorName}`);
        return null;
    }
    
    return new Constructor();
}

严格模式下的最佳实践 :使用 === undefined 进行精确检查,避免隐式类型转换。

实战场景一:泛型工厂模式

在严格模式中,结合泛型使用 Record 可以创建高度可复用的工厂:

TypeScript 复制代码
// 定义可构造的接口(严格模式要求明确类型)
interface IConstructable<T> {
    new (...args: any[]): T;
}

// 泛型工厂类
class StrictFactory<T> {
    private constructorMap: Record<string, IConstructable<T>>;

    constructor(map: Record<string, IConstructable<T>>) {
        this.constructorMap = map;
    }

    create<K extends string>(key: K): T | null {
        const Constructor = this.constructorMap[key];
        // 严格模式下的空值检查
        return Constructor !== undefined ? new Constructor() : null;
    }

    // 严格模式:提供类型守卫
    isKeyValid(key: string): key is keyof typeof this.constructorMap {
        return key in this.constructorMap;
    }
}

// 使用示例
const editorFactory = new StrictFactory<DriveBehaviorEditor>({
    "position": DriveLerpPositionEditor,
    "rotation": DriveLerpRotationEditor
});

// 严格模式下的安全调用
const key = "position";
if (editorFactory.isKeyValid(key)) {
    const editor = editorFactory.create(key); // 类型安全
}

实战场景二:不可变配置管理

在严格模式中,配置对象通常需要不可变性:

TypeScript 复制代码
type Environment = "development" | "staging" | "production";

interface ApiConfig {
    baseUrl: string;
    timeout: number;
    retries: number;
}

// 使用 readonly 和 as const 确保不可变
const apiConfigs: Record<Environment, Readonly<ApiConfig>> = {
    development: {
        baseUrl: "http://localhost:3000",
        timeout: 5000,
        retries: 0
    },
    staging: {
        baseUrl: "https://staging.api.com",
        timeout: 10000,
        retries: 1
    },
    production: {
        baseUrl: "https://api.com",
        timeout: 15000,
        retries: 3
    }
} as const; // as const 确保最深层次的不可变性

// 严格模式下访问
function getApiConfig(env: Environment): ApiConfig {
    const config = apiConfigs[env];
    // 由于 Record 的键是精确的 Environment 类型,
    // config 不可能是 undefined
    return {
        baseUrl: config.baseUrl,
        timeout: config.timeout,
        retries: config.retries
    };
}

实战场景三:权限矩阵

在严格模式中构建权限系统时,Record 的优势尤为明显:

TypeScript 复制代码
type Role = "admin" | "editor" | "viewer";
type Resource = "post" | "comment" | "user";
type Action = "create" | "read" | "update" | "delete";

// 三层嵌套的 Record 结构
type Permissions = Record<
    Role, 
    Record<
        Resource, 
        Partial<Record<Action, boolean>>
    >
>;

// 严格模式下必须完整定义所有 Role 和 Resource
const permissions: Permissions = {
    admin: {
        post: { create: true, read: true, update: true, delete: true },
        comment: { create: true, read: true, update: true, delete: true },
        user: { create: true, read: true, update: true, delete: true }
    },
    editor: {
        post: { create: true, read: true, update: true, delete: false },
        comment: { create: true, read: true, update: true, delete: true },
        user: { create: false, read: true, update: false, delete: false }
    },
    viewer: {
        post: { create: false, read: true, update: false, delete: false },
        comment: { create: false, read: true, update: false, delete: false },
        user: { create: false, read: false, update: false, delete: false }
    }
} as const;

// 严格模式下的安全检查函数
function canPerform(
    role: Role,
    resource: Resource,
    action: Action
): boolean {
    const rolePerms = permissions[role];
    const resourcePerms = rolePerms[resource];
    const actionPerm = resourcePerms[action];
    
    // 必须明确处理 undefined
    return actionPerm === true;
}

严格模式下的类型收窄

在使用 Record 时,严格模式要求我们进行显式的类型收窄:

TypeScript 复制代码
// 错误示例:在严格模式下可能导致问题
function naiveAccess(record: Record<string, number>, key: string): number {
    return record[key]; // 错误:可能返回 undefined
}

// 正确做法:显式检查
function safeAccess(record: Record<string, number>, key: string): number | undefined {
    return record[key]; // 正确:明确返回类型包含 undefined
}

// 更安全的做法:使用类型守卫
function hasKey<K extends string>(
    record: Record<string, any>,
    key: K
): record is Record<K, any> & typeof record {
    return key in record;
}

function narrowAccess(record: Record<string, number>, key: string): number | null {
    if (hasKey(record, key)) {
        return record[key]; // TypeScript 现在知道 key 一定存在
    }
    return null;
}

性能考量与最佳实践

在严格模式下使用 Record 时,需要注意:

  1. 对象查找 vs SwitchRecord 使用哈希查找,时间复杂度 O(1),在 case 较多时性能优于 switch 的 O(n)。

  2. 内存占用Record 作为常量映射在内存中是稳定的,而 switch 语句每次都会重新执行。

  3. ** tree-shaking 友好**:将 Record 定义为静态常量有助于编译器优化。

TypeScript 复制代码
// 推荐:静态只读映射(严格模式下最优)
private static readonly BEHAVIOR_MAP = {
    "DriveLerpPositionEditor": DriveLerpPositionEditor,
    "DriveLerpRotationEditor": DriveLerpRotationEditor,
    // ...
} as const;

// 如果需要更复杂的初始化,使用静态代码块(严格模式安全)
private static readonly BEHAVIOR_MAP: Record<string, DriveBehaviorEditorConstructor>;

static {
    const map: Record<string, DriveBehaviorEditorConstructor> = {};
    // 动态填充逻辑...
    this.BEHAVIOR_MAP = map;
}

总结:Record 在严格模式下的价值

通过这次重构,我们看到了 Record 在 TypeScript 严格模式中的核心价值:

  1. 类型安全:精确的键类型检查,避免魔法字符串

  2. 不可变性 :结合 readonlyas const 防止意外修改

  3. 空值安全:强制处理 undefined,避免运行时错误

  4. 代码简洁:声明式替代命令式,降低圈复杂度

  5. 易于测试:纯数据结构比 switch 语句更容易单元测试

  6. 符合 SRP:映射关系与创建逻辑分离,符合单一职责原则

在严格模式下,Record 不仅是类型工具,更是帮助我们写出更安全、更可维护代码的设计模式。它强迫我们直面可能的空值,显式处理边界情况,最终让我们的应用在编译期就捕获更多潜在错误。

下次当我们准备写超过 3 个 case 的 switch 语句时,不妨停下来问问自己:这个映射关系,是不是可以用 Record 来表达?

相关推荐
Wect2 小时前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
Dilettante2582 小时前
这一招让 Node 后端服务启动速度提升 75%!
typescript·node.js
jonjia19 小时前
模块、脚本与声明文件
typescript
jonjia19 小时前
配置 TypeScript
typescript
jonjia19 小时前
TypeScript 工具函数开发
typescript
jonjia19 小时前
注解与断言
typescript
jonjia19 小时前
IDE 超能力
typescript
jonjia19 小时前
对象类型
typescript
jonjia19 小时前
快速搭建 TypeScript 开发环境
typescript
jonjia19 小时前
TypeScript 的奇怪之处
typescript