You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 23:45:37