React+TypeScript如何强制要求props传入的回调为memoized类型
结论与实现方案
首先明确:TypeScript 无法原生识别普通匿名函数和useCallback包裹的记忆化函数的类型差异,两者的函数签名完全一致,不存在天然的类型标识可以用于区分。你要求的Memoized<T>类型校验可以通过手动打标记的方式实现,也可以通过调整组件逻辑从根源规避这个问题。
方案1:手动打标记实现类型校验
给记忆化函数添加独有属性做类型区分,只有符合标记要求的函数才能通过类型校验:
// 定义带独有标记的记忆化函数类型 type Memoized<T extends (...args: any[]) => any> = T & { __isMemoized: true } // 封装自定义hook,代替原生useCallback,自动给返回值打标记 function useMemoizedCallback<T extends (...args: any[]) => any>( callback: T, deps: React.DependencyList ): Memoized<T> { const cb = React.useCallback(callback, deps) as Memoized<T> cb.__isMemoized = true return cb } // 组件props类型调整 const DataRenderer = (props: { asyncDataFn: Memoized<() => Promise<object[]>> }) => { const [data, setData] = useState<object[]>([]); useEffect(() => { props.asyncDataFn().then(r => setData(r)) }, [props.asyncDataFn]); // ...其余逻辑 }
此时调用方如果直接传入匿名函数,TypeScript会直接抛出错误:类型 "() => Promise<{...}>" 上不存在属性 "__isMemoized",只有用useMemoizedCallback包裹的函数才能正常传入。
方案2:调整组件逻辑,免除调用方记忆化要求
这是更友好的组件设计思路,不用强制要求调用方处理函数记忆化,从组件内部解决无限执行问题:
const DataRenderer = (props: { asyncDataFn: () => Promise<object[]> }) => { const [data, setData] = useState<object[]>([]); // 用ref缓存最新的asyncDataFn引用 const asyncDataFnRef = useRef(props.asyncDataFn); // 每次props更新时同步ref内容 useEffect(() => { asyncDataFnRef.current = props.asyncDataFn }, [props.asyncDataFn]) // 主逻辑useEffect依赖留空,永远调用最新的函数,不会触发重复执行 useEffect(() => { asyncDataFnRef.current().then(r => setData(r)) }, []); // ...其余逻辑 }
这种实现下,即使调用方传入匿名函数,也不会出现useEffect无限执行的问题,不需要对调用方做额外约束。
内容的提问来源于stack exchange,提问作者Edwin Ries
相关产品推荐
相关产品推荐

