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

React中如何用Typestate模式消除异步数据的空值校验?

利用TypeScript可辨识联合类型实现编译期状态校验

针对React组件中异步加载数据导致的大量空值判断问题,可以借鉴Typestate模式,通过TypeScript的可辨识联合类型将组件的运行时状态映射到类型系统,让未加载的字段访问在编译期直接报错,无需手动空值校验。

核心思路

将组件状态拆分为多个不同的加载阶段,每个阶段用一个带唯一标识(如status)的接口定义:已加载的字段设为非空类型,未加载的设为null。通过标识字段触发TypeScript的类型缩小,确保在不同阶段只能访问对应已加载的字段。

类组件实现示例

1. 定义状态类型

首先定义所有字段的基础类型,再拆分不同加载阶段的状态:

// 所有字段的最终非空类型
interface FullData {
  summary: string;
  leftContents: string;
  rightContents: string;
  number: number;
}

// 初始加载状态:所有字段为null
type LoadingState = {
  status: 'loading';
} & Record<keyof FullData, null>;

// Summary加载完成状态:summary非空,其余为null
type SummaryLoadedState = {
  status: 'summary-loaded';
  summary: string;
  leftContents: null;
  rightContents: null;
  number: null;
};

// LeftContents加载完成状态:summary和leftContents非空
type LeftLoadedState = {
  status: 'left-loaded';
  summary: string;
  leftContents: string;
  rightContents: null;
  number: null;
};

// 完全加载状态:所有字段非空
type FullLoadedState = {
  status: 'full-loaded';
} & FullData;

// 组件状态为所有阶段的联合类型
type ComponentState = LoadingState | SummaryLoadedState | LeftLoadedState | FullLoadedState;

2. 编写组件逻辑

将组件的state泛型指定为上述联合类型,初始化时为loading状态,异步请求完成后切换对应状态:

interface MyProps {
  id: number;
}

class MyComponent extends React.Component<MyProps, ComponentState> {
  state: ComponentState = {
    status: 'loading',
    summary: null,
    leftContents: null,
    rightContents: null,
    number: null,
  };

  componentDidMount() {
    // 加载summary,切换到summary-loaded状态
    fetch(`/api/${this.props.id}/summary`)
      .then(res => res.json())
      .then(data => {
        this.setState({
          status: 'summary-loaded',
          summary: data.summary,
          leftContents: null,
          rightContents: null,
          number: null,
        });
      });

    // 加载leftContents,根据当前状态切换到对应阶段
    fetch(`/api/${this.props.id}/leftContents`)
      .then(res => res.json())
      .then(data => {
        this.setState(prevState => {
          if (prevState.status === 'summary-loaded') {
            return {
              status: 'left-loaded',
              summary: prevState.summary,
              leftContents: data.leftContents,
              rightContents: null,
              number: null,
            };
          }
          // 若还在初始加载状态,直接更新leftContents并切换状态
          return {
            ...prevState,
            status: 'left-loaded',
            leftContents: data.leftContents,
          };
        });
      });
  }

  render() {
    switch (this.state.status) {
      case 'loading':
        return <div>加载中...</div>;
      case 'summary-loaded':
        // 此处this.state.summary必为string,无需空值判断
        return (
          <div>
            <h1>{this.state.summary}</h1>
            <div>加载左侧内容中...</div>
          </div>
        );
      case 'left-loaded':
        // summary和leftContents均为非空,可安全访问
        return (
          <div>
            <h1>{this.state.summary}</h1>
            <div>{this.state.leftContents}</div>
            <div>加载剩余数据中...</div>
          </div>
        );
      case 'full-loaded':
        // 所有字段均可用
        return (
          <div>
            <h1>{this.state.summary}</h1>
            <div>{this.state.leftContents}</div>
            <div>{this.state.rightContents}</div>
            <div>{this.state.number}</div>
          </div>
        );
    }
  }
}

函数组件优化方案

函数组件中使用useReducer更适合管理多阶段状态,逻辑更清晰:

import { useReducer, useEffect } from 'react';

// 复用之前定义的FullData和ComponentState类型
type Action =
  | { type: 'SUMMARY_LOADED'; payload: string }
  | { type: 'LEFT_CONTENTS_LOADED'; payload: string }
  | { type: 'FULL_DATA_LOADED'; payload: FullData };

const reducer = (state: ComponentState, action: Action): ComponentState => {
  switch (action.type) {
    case 'SUMMARY_LOADED':
      return {
        status: 'summary-loaded',
        summary: action.payload,
        leftContents: null,
        rightContents: null,
        number: null,
      };
    case 'LEFT_CONTENTS_LOADED':
      if (state.status === 'loading') {
        return {
          status: 'left-loaded',
          summary: null,
          leftContents: action.payload,
          rightContents: null,
          number: null,
        };
      }
      return { ...state, status: 'left-loaded', leftContents: action.payload };
    case 'FULL_DATA_LOADED':
      return { status: 'full-loaded', ...action.payload };
    default:
      return state;
  }
};

const MyComponent = ({ id }: { id: number }) => {
  const [state, dispatch] = useReducer(reducer, {
    status: 'loading',
    summary: null,
    leftContents: null,
    rightContents: null,
    number: null,
  });

  useEffect(() => {
    fetch(`/api/${id}/summary`)
      .then(res => res.json())
      .then(data => dispatch({ type: 'SUMMARY_LOADED', payload: data.summary }));

    fetch(`/api/${id}/leftContents`)
      .then(res => res.json())
      .then(data => dispatch({ type: 'LEFT_CONTENTS_LOADED', payload: data.leftContents }));
  }, [id]);

  switch (state.status) {
    case 'loading':
      return <div>加载中...</div>;
    case 'summary-loaded':
      return <div>摘要:{state.summary}</div>;
    case 'left-loaded':
      return (
        <div>
          {state.summary && <div>摘要:{state.summary}</div>}
          <div>左侧内容:{state.leftContents}</div>
        </div>
      );
    case 'full-loaded':
      return (
        <div>
          <div>摘要:{state.summary}</div>
          <div>左侧内容:{state.leftContents}</div>
          <div>右侧内容:{state.rightContents}</div>
          <div>数值:{state.number}</div>
        </div>
      );
  }
};

关键优势

  1. 编译期校验:在未加载阶段访问对应字段会直接触发TypeScript编译错误,从根源避免空值判断。
  2. 状态清晰:每个状态阶段对应明确的字段可用性,代码可读性更强。
  3. 类型自动缩小:通过switch或类型守卫,TypeScript会自动推断当前状态下的可用字段,无需手动类型断言。

内容的提问来源于stack exchange,提问作者Nils

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 20:45:13