【TS】优雅的使用泛型推导
前言
JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据。因此,js的变量声明,只需选择合适的修饰符,不需要显式声明类型。
在编写 JavaScript 代码的时候,如何使用一个变量或调用一个函数,这些问题的答案经常需要我们自己记在脑子里,而且我们必须得祈祷自己处理好了所有细节。要让纯 JavaScript 告诉我们fn
在给定特定参数的时候会做什么事,唯一的方法就是实际调用fn
函数。
这样的行为使得我们很难在代码执行前进行相关的预测,也意味着我们在编写代码的时候,很难搞清楚代码会做什么事。
本文不会细讲ts的用法,只是介绍两种使用泛型后可以完美限定及推导参数的场景。
场景一:事件总线
一个稍大型的项目里,或多或少,都会有事件总线的身影。跟普通的变量或对象使用interface
或type
定义不同,事件总线有不同的EventName
及其对应的参数。
正常使用时,每次emit
前,都要去接收方看下参数是啥,同理,每次on
前,都要看下发送方是啥,一不小心就会多一个或少一个参数,或者变量名写错了。
使用TS限定事件总线势在必行~
const bus = new EventEmitter();
interface EventNameAndProps {
composition_change: { isComposing: boolean };
editor_focus: { focus: boolean };
}
interface PropEventSource<T> {
on: <Key extends string & keyof T>(
eventName: Key,
handler: (opts: T[Key]) => void
) => Function;
off: <Key extends string & keyof T>(
eventName: Key,
handler: (opts: T[Key]) => void
) => void;
emit: <Key extends string & keyof T>(eventName: Key, opts: T[Key]) => void;
}
export const Bus: PropEventSource<EventNameAndProps> = {
on: (eventName, handler) => {
bus.on(eventName, handler);
return () => {
bus.off(eventName, handler);
};
},
emit: (eventName, opts) => {
bus.emit(eventName, opts);
},
off: (eventName, handler) => {
bus.off(eventName, handler);
},
};
这样一来,EventName被限定了,同时,opts的参数在确定是哪个EventName后,会自动推导出对应的类型,并给出语法提示。
Bus.emit('composition_change', { isComposing: true }); // 语法提示会直接告诉你在该eventName下应该传什么变量
Bus.emit('editor_focus', { focus: true }); // 不同的eventName会自动提示不同的变量
不管是发送方还是接收方,如果事件名及对应参数不匹配,就会给出错误提示,完美~
还不赶紧拿到本地试试?
useReduce同样可以使用,通过限定dispatch,来实现调用的时候自动推导type及其对应参数
场景二:useContext优化
在函数式组件中,使用useContext需要谨慎,当所引用的context发生变化时,该函数组件会自动重新执行,哪怕在函数组件外使用memo
包裹起来也不行。
但是,如果重新声明一个函数,专门包裹一层context,用于获取组件需要的属性,由于ts声明问题以及组件可能引用不同的context,每个组件都要有一一对应的包裹函数,实现成本太高。
使用TS泛型及自动推导能力,定义了通用的高阶组件。
下面的示例中,组件A
需要外部传参a
,需要从EditorContext中获取initialized
。
如果直接在组件A中使用useContext
,A会由于EditorContext中其他数据的变化,导致多次无效的更新,具体次数取决于context
中其他变量的变化频率。
import { EditorContext } from '@/contexts/EditorContext';
import React, { FunctionComponent, memo, useContext, useMemo } from 'react';
const A = memo(({ a, initialized }: { a: number; initialized: boolean; }) => {
console.log('renderA', initialized);
return <div>123</div>;
});
function withContext<T, K extends string & keyof T, P extends Pick<T, K>>(
context: React.Context<T>,
keys: K[],
Component: FunctionComponent<P>
) {
return (props: Omit<P, K>) => {
const Comp = useMemo(() => { return memo(Component); }, []);
const value = useMemo(() => { const opts: Pick<T, K> = {} as any; keys.forEach((k) => { opts[k] = d[k]; }); return opts; }, [d]);
console.log('renderContext', value);
// @ts-ignore return <Comp {...props} {...value} />;
};
}
const S = withContext(EditorContext, ['initialized'], A); // 该组件可连续嵌套
export function THooks() {
return <S a={1} />;
}
使用高阶组件withContext自动推导组件A
所需参数,在withContext内部获取所需context
变量,通过memo
避免组件A
的多次渲染,一个优化组件就这么完成了。
如果组件需要从多个context中获取变量,只需连续嵌套使用withContext即可。
出于某些考虑,我们项目不再使用redux或mobx进行状态管理,而是在顶层使用context存储一些全局变量,其他状态尽可能下放到子组件中,因此才有的这个高阶函数。
给个赞或关注再走吧~
转载自:https://juejin.cn/post/7208941394391220280