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> ); } };
关键优势
- 编译期校验:在未加载阶段访问对应字段会直接触发TypeScript编译错误,从根源避免空值判断。
- 状态清晰:每个状态阶段对应明确的字段可用性,代码可读性更强。
- 类型自动缩小:通过
switch或类型守卫,TypeScript会自动推断当前状态下的可用字段,无需手动类型断言。
内容的提问来源于stack exchange,提问作者Nils
相关产品推荐
相关产品推荐

