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 来表达?

相关推荐
克喵的水银蛇1 小时前
Flutter 弹性布局实战:Row/Column/Flex 核心用法与优化技巧
前端·javascript·typescript
Beginner x_u2 小时前
Vue3 + TS + TailwindCSS 操作引导组件开发逐行解析
typescript·vue3·前端开发·tailwindcss·组件开发
by__csdn3 小时前
ES6新特性全攻略:JavaScript的现代革命
开发语言·前端·javascript·typescript·ecmascript·es6·js
by__csdn3 小时前
Vue 双向数据绑定深度解析:从原理到实践的全方位指南
前端·javascript·vue.js·typescript·前端框架·vue·ecmascript
by__csdn4 小时前
Vue3响应式系统详解:ref与reactive全面解析
前端·javascript·vue.js·typescript·ecmascript·css3·html5
喵个咪14 小时前
初学者入门:用 go-kratos-admin + protoc-gen-typescript-http 快速搭建企业级 Admin 系统
后端·typescript·go
初遇你时动了情20 小时前
react native创建项目常用插件
react native·typescript·reactjs
时71 天前
利用requestIdleCallback优化Dom的更新性能
前端·性能优化·typescript
lcc1871 天前
Typescript 类的简介
typescript