如何在React组件中实现TypeScript类型收窄?
React组件封装中的TypeScript类型收窄问题
场景与初始实现
我在代码里频繁用三元表达式做类型收窄,方便在JSX里使用对应值,比如:
const user: { name: { first: string, last: string } } | null = /* 数据源 */; return ( <div> {user ? ( <> <span>{user.name.first}</span> <span>{user.name.last}</span> </> ) : <LoadingSpinner />} </div> );
封装组件的尝试与问题
我想封装一个组件替代重复代码,自动实现过滤逻辑,示例如下:
const Loading = ({ loading, children }: { loading?: boolean, children: React.ReactNode }) => { if (loading) return <LoadingSpinner />; return <> {children} </>; }; // 使用方式 return ( <div> <Loading loading={!user}> <span>{user.name.first}</span> <span>{user.name.last}</span> </Loading> </div> );
但问题在于,TypeScript不会把这个组件当成类型守卫/类型收窄工具,会提示user可能为null——这其实合理,因为当user为null时,JSX子元素会报错。我想找一种简洁通用的方式创建这类组件,既能实现类型收窄又能避免错误(不局限于特定props)。目前我想到的方案需要大量函数包装和props(用Required和NonNullable保证安全通用),但不确定替换所有现有代码是否值得,因为可能会更冗长。
强类型方案实现(更新1)
后来我意识到组件没法直接做类型守卫,于是基于思路实现了一个适配场景的强类型方案:
type PartialOrNull<T> = { [K in keyof T]+?: T[K] | null; }; type RequiredNotNull<T> = { [P in keyof T]-?: NonNullable<T[P]>; }; type RequiredRenderFunction<T> = (data: RequiredNotNull<T>) => React.ReactNode; type NotRequiredRenderFunction<T> = (data?: T | null) => React.ReactNode; interface LoadingProps<T extends object> { loading?: boolean; data?: T | PartialOrNull<T> | null; render: RequiredRenderFunction<T> | NotRequiredRenderFunction<T>; require?: boolean; } const notUndefinedOrNull = <V>(value: V | null | undefined): value is V => !(value === null || value === undefined); function Loading<T extends object>({ loading = false, data, render, require = false }: LoadingProps<T>) { if (loading) { return <LoadingSpinner />; } if (!require) { return (<>{(render as NotRequiredRenderFunction<T>)(data)}</>); } if (!data) return null; const values = Object.values(data); if (!values.length || values.filter(notUndefinedOrNull).length !== values.length) { return null; } return (<>{(render as RequiredRenderFunction<T>)(data as RequiredNotNull<T>)}</>); }
使用示例
interface User { name: { first: string; last: string; } }; const UserComponent = ({ user }: { user: User }) => { return ( <> <span>{user.name.first}</span> <span>{user.name.last}</span> </> ); }; // 组件使用 const App = () => { const user: User | null = useGetUser(); return ( <div> <Loading loading={!user} require data={{ user }} render={UserComponent} /> </div> ); };
内容的提问来源于stack exchange,提问作者Shoham Ben Shitrit
相关产品推荐
相关产品推荐

