【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);
              }));
}

相关推荐
bug丸6 分钟前
v8引擎垃圾回收
前端·javascript·垃圾回收
安全小王子8 分钟前
攻防世界web第三题file_include
前端
&活在当下&9 分钟前
ref 和 reactive 的用法和区别
前端·javascript·vue.js
百事老饼干12 分钟前
VUE前端实现防抖节流 Lodash
前端
web Rookie16 分钟前
React 高阶组件(HOC)
前端·javascript·react.js
云白冰30 分钟前
hiprint结合vue2项目实现静默打印详细使用步骤
前端·javascript·vue.js
葡萄架子38 分钟前
Python中的logger作用(from loguru import logger)
java·前端·python
Hi_MrXiao1 小时前
前端实现图片压缩插件(image-compressorionjs)
前端
阿智@111 小时前
Node.js 助力前端开发:自动化操作实战
运维·前端·node.js·自动化
m0_748251721 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·vue.js·ajax