【React】一些实际项目中的 TypeScript 技巧(三)~

1、获取函数的参数类型

起因是这样的,我在使用 Arco Design Form 表单的时候,希望获取到 onFormSubmit 的参数类型,于是查看了其类型声明文件:

ts 复制代码
export interface FormProviderProps {
    /**
     * @zh 包裹的任意 `Form` 组件的值改变时,该方法会被调用
     * @en This method is called when the value of any wrapped `Form` component changes
     */
    onFormValuesChange?: (id: string | undefined, changedValues: any, { forms, }: {
        forms: {
            [key: string]: FormInstance;
        };
    }) => void;
    /**
     * @zh 包裹的任意 `Form` 组件触发提交时,该方法会被调用
     * @en This method will be called when any wrapped `Form` component triggers a submit
     */
    onFormSubmit?: (id: string | undefined, values: any, { forms, }: {
        forms: {
            [key: string]: FormInstance;
        };
    }) => void;
}

原本以为下面的方式就可以,但是报错了:

这个错误是因为Parameters类型要求传入的参数是一个函数类型,但是FormProviderProps['onFormSubmit']可能是一个undefined

为了解决这个问题,可以使用条件类型(Conditional Types)来检查onFormSubmit是否为函数类型,然后再提取参数类型。修改后的代码如下:

ts 复制代码
type OnFormSubmitParams = FormProviderProps['onFormSubmit'] extends (
  ...args: infer Args
) => any
  ? Args
  : never;

type OnFormSubmitReturn = FormProviderProps['onFormSubmit'] extends (
  ...args: any
) => infer Return
  ? Return
  : never;

使用了条件类型FormProviderProps['onFormSubmit'] extends (...args: infer Args) => any ? Args : never来检查onFormSubmit是否为函数类型。如果是函数类型,则提取参数类型为Args,否则为never

在上述代码中,ArgsReturn都是类型变量,可以理解为占位符。它们的具体含义如下:

  • Args: 这是一个用于存储函数参数类型的类型变量。通过infer关键字,它可以从函数类型中提取参数类型。
  • Return: 这是一个用于存储函数返回类型的类型变量。同样通过infer关键字,它可以从函数类型中提取返回类型。

infer关键字用于条件类型中,可以提取出某个类型的具体类型。在这里,infer Args表示从函数类型中提取参数类型,并将其赋值给Args。同样,infer Return表示从函数类型中提取返回类型,并将其赋值给Return

通过使用infer关键字,我们可以在条件类型中提取出具体的类型,并将其赋值给类型变量。这样可以在后续的代码中使用这些具体的类型。在我们的情况下,Args用于存储函数的参数类型,Return用于存储函数的返回类型。

2、妙用 Object.entries()

假如我们有一个对象及其类型:

ts 复制代码
const alphas = {
  a: 'ds,dss',
  b: 'ds,dssds',
  c: 'dsds,dsd',
  d: 'dsd,dsdds ',
};

interface IAlphas {
  a: string;
  b: string;
  c: string;
  d: string;
}

在业务开发中,我们希望遍历这个对象,然后逐个渲染在页面上。通常会这么做:

tsx 复制代码
<>
    {alphas &&
      Object.keys(alphas).map(key => (
        <div key={key}>
          {key}:{alphas[key as keyof IAlphas]}
        </div>
      ))}
</>

如果不断言 key 的类型,则会出现下面的报错:

如果用 Object.entries() 则更好,省去了我们强行去断言 key 的类型:

tsx 复制代码
<>
    {alphas && Object.entries(alphas).map(([key, value]) => (
      <div key={key}>
        {key}: {value}
      </div>
    ))}
</>

3、switch与对象字面量

switch 语句在多种编程语言中都很常用。然而,作为一名前端,我总是更喜欢对象字面。为什么呢?使用 switch 语句,我们有一个程序化的控制流,并被迫在每个 case 块中使用 break。如果忘记这样做,可能会导致错误或意想不到的行为。

另一方面,对象字面形式的控制流更自然,更易于维护,可读性也更好。

ts 复制代码
handleAction(type: ActionType, task: Task): void {w
    switch(type) {
        case ActionType.NEW_TASK:
            this.taskService.createTask(task);
            break;
        case ActionType.EDIT_TASK:
            this.taskService.editTask(task);
            break;
        case ActionType.REMOVE_TASK:
            this.taskService.removeTask(task);
            break;
        case ActionType.COMPLETE_TASK:
            this.taskService.completeTask(task);
            break;
        default:
            throw Error(`handleAction: taskActionType ${type} is not supported`);
    }
}

在我看来,对象文字量是一种更简洁的方法。操作结构合理,可扩展性更强。

ts 复制代码
handleAction(type: ActionType, task: Task): void {
    const actions = <Record<ActionType, (task: Task) => Promise<void>>>{
        [ActionType.NEW_TASK]: (task: Task) =>
            this.taskService.createTask(task),
        [ActionType.EDIT_TASK]: (task: Task) =>
            this.taskService.editTask(task),
        [ActionType.REMOVE_TASK]: (task: Task) =>
            this.taskService.removeTask(task),
        [ActionType.COMPLETE_TASK]: (task: Task) =>
            this.taskService.completeTask(task),
    };
    if (actions[type]) {
        void actions[type](task);
    } else {
        throw Error(`handleAction: taskActionType ${type} is not supported`);
    }
}

4、Map vs 对象字面量

我们都熟悉 JavaScript 中 Map 的基本定义:

ts 复制代码
const taskLabelMap = new Map([
    [ActionType.NEW_TASK, 'Create Task'],
    [ActionType.EDIT_TASK, 'Edit Task'],
    [ActionType.REMOVE_TASK, 'Remove Task'],
    [ActionType.COMPLETE_TASK, 'Complete Task'],
]);

这在维护、定义和一目了然的阅读方面都有些复杂。有时,开发会忘记在 Typescript 中定义 Map 还有更简单的方法,即使用对象字面量。使用给定的 JavaScript 特性比使用 Typescript 引入的数据结构感觉更自然。

ts 复制代码
const taskLabelMap1: {[key: string]: string} = {
    [ActionType.NEW_TASK]: 'Create Task',
    [ActionType.EDIT_TASK]: 'Edit Task',
    [ActionType.REMOVE_TASK]: 'Remove Task',
    [ActionType.COMPLETE_TASK]: 'Complete Task',
}

不过,也有一些例外情况,我更喜欢 Typescript 提供的数据结构。所有这些都与性能有关。因此,如果一个人可能会有一个大的 item 列表,并且必须对它们进行一些操作,那么首先看看 Map 可能是明智之举。

5、Enum vs 对象字面量和字符串联合类型

JavaScript 中没有枚举。

因此,使用 Object literals 感觉更自然。那么,有什么区别呢?枚举具有一些额外的功能。与对象字面量相比,枚举的值不能赋给不同的值。

因此,为了保证类型安全,Object 字面量需要与 typeof 功能结合使用。

ts 复制代码
export enum ActionType {
    NEW_TASK = 'new',
    EDIT_TASK = 'edit',
    REMOVE_TASK = 'remove',
    COMPLETE_TASK = 'complete',
}

很高兴知道这一点: 如果在枚举中添加 const,运行时枚举将不存在,而是编译为对象字面。

下面的示例使用了 typeof 功能。这些值是类型安全的,但不能像使用枚举那样显式地使用这些值。

ts 复制代码
const taskActions = <const>{
    NEW_TASK: 'CREATE',
    EDIT_TASK: 'EDIT',
    REMOVE_TASK: 'REMOVE',
    COMPLETE_TASK: 'COMPLETE',
};
type TASK_ACTIONS = (typeof taskActions)[keyof typeof taskActions];

因此,枚举的最佳替代品是结合类声明的联合类型。这些值是类型安全的,并且可以像枚举一样被引用。

ts 复制代码
const newTask = ActionType.NEW_TASK
ts 复制代码
type TASK_ACTION = 'CREATE' | 'EDIT' | 'REMOVE' | 'COMPLETE';
export class TaskActions {
    NEW_TASK: TASK_ACTION = 'CREATE';
    EDIT_TASK: TASK_ACTION = 'EDIT';
    REMOVE_TASK: TASK_ACTION = 'REMOVE';
    COMPLETE_TASK: TASK_ACTION = 'COMPLETE';
}

6、For 循环 vs Reduce

JavaScript 中有许多可用的数组函数。因此,每个前端都应该熟悉这些函数。但是,开发往往使用不同的方法来实现基本任务。在下面的示例中,应检索具有特定类别的任务数。

可以使用 for 循环,如下所示:

ts 复制代码
let taskHomeCount = 0;
for (const task of tasks) {
    if (task.category === TaskCategroy.HOME) {
        taskHomeCount += 1;
    }
}

不过,作为 JavaScript 开发,reduce 可以很容易地替代它。我个人就经常使用 reduce。数组可以轻松转换为各种类型的对象。这是一个非常强大的功能。

ts 复制代码
return taskHomeCount = tasks.reduce((result: number, task: Task) => {
    return task.category === TaskCategory.HOME ? ++result : result;
}, 0);

7、Promise vs Observable

当需要取消或延迟请求时,Observable 是个不错的选择,但如果不需要这些功能,Promises 也能胜任。

下面的代码片段展示了 Promise 的使用。设想一下,removeTask 返回一个可观察对象,而我们可能只需要触发 REST 调用来执行操作。那么,最好使用 Promise。

为什么呢?因为使用 Promise 时,我们不必关心订阅。

ts 复制代码
async removeTask(task: Task): Promise<void> {
     return await lastValueFrom(this.taskService.removeTask(task.taskId));
}

handleRemove(task: Task) {
     void this.removeTask(task);
}

在下面的示例中,使用的是 Observables。因此,无论我们喜欢与否,我们都必须处理订阅。

ts 复制代码
removeTask(task: Task): Observable<void> {
     return this.taskService.removeTask(task.taskId);
}

handleRemove(task: Task) {
    this.removeTask(task).pipe(first());
}

更高级的例子是在一个方法中使用多个 REST 调用。比方说,我们可能需要更改当前的可见页面,在这种情况下就是活动标签页。但是,这只能在第一个请求完成后进行。通过 Promises,我们可以轻松地使用 async 和 await。但是,对于可观察对象,我们必须使用 tap 操作符并处理订阅。

ts 复制代码
async loadTaskDetails(taskId: number): Promise<TaskDetails> {
    const taskDetails = await lastValueFrom(this.loadDetails(taskId));
    await this.setActiveTab(Tab.DETAILS);
    return taskDetails;
}

loadTaskDetails(taskId: number): Observable<TaskDetails> {
    return this.loadDetails(taskId)
              .pipe(first(), tap((taskDetails) => {
                  void this.setActiveTab(Tab.DETAILS);
              }));
}

相关推荐
无尽的沉默15 分钟前
Thymeleaf 表达式
java·开发语言·前端
无尽的沉默17 分钟前
Spring Boot 整合 Thymeleaf 模板引擎
java·前端·spring boot
We་ct23 分钟前
从输入URL到页面显示的完整技术流程
前端·edge·edge浏览器
先做个垃圾出来………40 分钟前
DeepDiff差异语义化特性
服务器·前端
蓝帆傲亦1 小时前
前端常用可视化图表组件大全
前端
CappuccinoRose2 小时前
HTML语法学习文档(九)
前端·学习·架构·html5
NEXT062 小时前
BFC布局
前端·css·面试
菜鸟小芯2 小时前
【GLM-5 陪练式前端新手入门】第四篇:卡片布局 —— 让个人主页内容更有层次
前端·人工智能
Hello.Reader2 小时前
Leptos + Tauri 2 前端配置Trunk + SSG + 移动端热重载一次打通(Leptos 0.6 口径)
前端
岱宗夫up2 小时前
【前端基础】HTML + CSS + JavaScript 进阶(一)
开发语言·前端·javascript·css·html