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
。
在上述代码中,Args
和Return
都是类型变量,可以理解为占位符。它们的具体含义如下:
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);
}));
}