React/TypeScript:处于上下文API状态的联合类型


当我在其他组件中使用状态时,TypeScript似乎没有识别出属性state.recipes确实存在,如果YummlyStateRecipesState的类型,就会出现这种情况。我怀疑YummlyState总是InitialState的类型,因为由于初始状态被设置,这是它最初的类型。

还包括,关于这个上下文,你是否注意到了其他你认为应该有所不同的东西?

非常感谢!

import React, {
createContext,
Dispatch,
PropsWithChildren,
ReactElement,
Reducer,
useContext,
useReducer,
} from 'react'
//  Recipe
export type Recipe = {
id: number
title: string
image: string
readyInMinutes: number
diets: string[]
pricePerServing: number
servings: number
}
// Response
export type SpoonacularResponse = {
number: number
offset: number
results: Recipe[]
totalResults: number
}
// Yummly State
type StatusUnion = 'resolved' | 'rejected' | 'idle' | 'pending'
type InitialState = {
status: StatusUnion
}
type SingleRecipeState = InitialState & {
recipe: Recipe
}
type RecipesState = InitialState & {
recipes: Recipe[]
}
type ErrorState = InitialState & {
error: unknown
}
type YummlyState = InitialState | SingleRecipeState | RecipesState | ErrorState
// Action Union Type for the reducer
type Action =
| { type: 'pending' }
| { type: 'singleRecipeResolved'; payload: Recipe }
| { type: 'recipesResolved'; payload: Recipe[] }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| { type: 'rejected'; payload: unknown }
// The initial state
const initialState: YummlyState = {
status: 'idle',
}
//  The Reducer
function yummlyReducer(state: YummlyState, action: Action): YummlyState {
switch (action.type) {
case 'pending':
return {
status: 'pending',
}
case 'singleRecipeResolved':
return {
...state,
status: 'resolved',
recipe: action.payload,
}
case 'recipesResolved':
return {
...state,
status: 'resolved',
recipes: action.payload,
}
case 'rejected':
return {
...state,
status: 'rejected',
error: action.payload,
}
default:
throw new Error('This should not happen :D')
}
}
type YummlyContextType = {
state: YummlyState
dispatch: Dispatch<Action>
}
const YummlyContext = createContext<YummlyContextType>({
state: initialState,
dispatch: () => {},
})
YummlyContext.displayName = 'YummlyContext'
// eslint-disable-next-line @typescript-eslint/ban-types
function YummlyProvider(props: PropsWithChildren<{}>): ReactElement {
const [state, dispatch] = useReducer<Reducer<YummlyState, Action>>(
yummlyReducer,
initialState
)
const value = { state, dispatch }
return <YummlyContext.Provider value={value} {...props} />
}
function useYummlyContext(): YummlyContextType {
const context = useContext(YummlyContext)
if (!context) {
throw new Error(`No provider for YummlyContext given`)
}
return context
}
export { YummlyProvider, useYummlyContext }

在处理联合时,您将无法访问state.recipes等属性,除非该属性已在联合的ALL成员上声明。基本上有两种方法可以处理这种类型的事情:

  1. 在尝试访问属性键之前,请检查该属性键是否存在。如果它存在,则我们知道它是一个有效值,而不是undefined

  2. YummlyState并集中包含一个基本接口,该接口表示可以访问任何成员的所有键,但它们的值可能是undefined

防护属性

在不更改类型定义的情况下,最简单的方法就是使用类型保护来查看属性是否存在。基于您的并集,typescript知道如果存在属性recipes,那么它的类型必须是Recipe[]

const Test = () => {
const {state, dispatch} = useContext(YummlyContext);
if ( 'recipes' in state ) {
// do something with recipes
const r: Recipe[] = state.recipes;
}
}

声明可选属性

我们希望包含在联盟中的基本接口如下:

interface YummlyBase {
status: StatusUnion;
recipe?: Recipe;
recipes?: Recipe[];
error?: unknown;
}

status是必需的,但所有其他属性都是可选的。这意味着我们总是可以访问它们,但它们可能是undefined。因此,在使用之前,您需要检查特定值是否不是undefined

我们能够破坏对象,这很好:

const base: YummlyBase = { status: 'idle' };
const {status, recipe, recipes, error} = base;

仅使用YummlyBase是可以的,但它并不能为我们提供所有信息。最好是YummlyState是基础特定成员的联合。

type YummlyState = YummlyBase & (InitialState | SingleRecipeState | RecipesState | ErrorState)

歧视工会

您的每个场景都有不同的status字符串文字(好吧,大部分(,但我们还没有利用这一事实。区分并集是一种基于字符串属性(如status(的值来缩小对象类型的方法。

您已经在使用Action并集类型执行此操作。当您基于action.type进行switch时,它知道action.payload的正确类型。

这在某些情况下会非常有帮助。这里的帮助不大,因为SingleRecipeStateRecipesState都使用状态resolved,所以您仍然需要额外的检查。这就是为什么我把这个选项放在最后。

type InitialState = {
status: 'idle' | 'pending';
}
type SingleRecipeState = {
status: 'resolved';
recipe: Recipe
}
type RecipesState = {
status: 'resolved';
recipes: Recipe[];
}
type ErrorState = {
status: 'rejected';
error: unknown;
}
type YummlyState = InitialState | SingleRecipeState | RecipesState | ErrorState
type StatusUnion = YummlyState['status'];
const check = (state: YummlyState) => {
if (state.status === 'rejected') {
// state is ErrorState
state.error;
}
if ( state.status === 'resolved' ) {
// state is RecipesState or SingleRecipeState
state.recipe; // still an error because we don't know if it's single or multiple
}
}

游乐场链接

相关内容

  • 没有找到相关文章

最新更新