[英]React/TypeScript: Union Types in state of Context API
TypeScript does not seem to be recognizing that property state.recipes
do exist when I use the state in some other component, this would be the case if YummlyState
is the type of RecipesState
.打字稿似乎并没有被认识到财产
state.recipes
当我在其他一些组件使用的状态确实存在,这将是情况下,如果YummlyState
是类型RecipesState
。 I suspect the YummlyState
to always be the type of InitialState
because that is the type it will have initially because of the initial state being set.我怀疑
YummlyState
永远是类型InitialState
,因为这是它会因为处于初始状态组的最初的类型。
Also to include, is there anything else you notice about this Context which you think should be different?还要包括,您是否注意到有关此上下文的其他任何您认为应该不同的地方?
Many thanks!非常感谢!
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 }
When dealing with a union, you will not be able to access a property such as state.recipes
unless that property has been declared on ALL members of the union.在处理联合时,您将无法访问诸如
state.recipes
的属性,除非该属性已在联合的所有成员上声明。 There are essentially two ways that you can deal with this type of thing:基本上有两种方法可以处理这种类型的事情:
Check that the property key exists before trying to access it.在尝试访问它之前检查属性键是否存在。 If it exists, we know it is a valid value and not
undefined
.如果它存在,我们就知道它是一个有效值而不是
undefined
。
Include a base interface in the YummlyState
union which says that all of the keys of any of the members can be accessed, but their values might be undefined
.在
YummlyState
联合中包含一个基本接口,它表示可以访问任何成员的所有键,但它们的值可能是undefined
。
Without changing your type definitions, the simplest thing you can do is use a type guard to see if a property exists.在不更改类型定义的情况下,您可以做的最简单的事情是使用类型保护来查看属性是否存在。 Based on your union, typescript knows that if there is a property
recipes
, it must be of type Recipe[]
.根据你的联合,打字稿知道如果有一个属性
recipes
,它必须是Recipe[]
类型。
const Test = () => {
const {state, dispatch} = useContext(YummlyContext);
if ( 'recipes' in state ) {
// do something with recipes
const r: Recipe[] = state.recipes;
}
}
The base interface that we want to include in our union looks like this:我们想要包含在我们的联合中的基本接口如下所示:
interface YummlyBase {
status: StatusUnion;
recipe?: Recipe;
recipes?: Recipe[];
error?: unknown;
}
status
is required, but all other properties are optional. status
是必需的,但所有其他属性都是可选的。 This means that we can always access them, but they might be undefined
.这意味着我们总是可以访问它们,但它们可能是
undefined
。 So you need to check that a particular value is not undefined
before using it.因此,您需要在使用之前检查特定值是否
undefined
。
We are able to destructure the object, which is nice:我们能够解构对象,这很好:
const base: YummlyBase = { status: 'idle' };
const {status, recipe, recipes, error} = base;
Using just the YummlyBase
alone is ok, but it doesn't give us all of the information.单独使用
YummlyBase
是可以的,但它并没有给我们所有的信息。 It's better if YummlyState
is the base and a union of specific members.如果
YummlyState
是特定成员的基础和联合, YummlyState
更好了。
type YummlyState = YummlyBase & (InitialState | SingleRecipeState | RecipesState | ErrorState)
Each of your scenarios has a different string literal for status
(well, mostly), but we haven't made use of that fact.您的每个场景都有不同的
status
字符串文字(好吧,大多数情况下),但我们没有利用这个事实。 Discriminating unions is a way to narrow the type of the object based on the value of a string property like status
. 区分联合是一种根据
status
等字符串属性的值来缩小对象类型的方法。
You are already doing this with your Action
union type.您已经在使用
Action
联合类型执行此Action
。 When you switch
based on action.type
, it knows the correct type for action.payload
.当您根据
action.type
switch
时,它知道action.payload
的正确类型。
This can be extremely helpful in some situations.这在某些情况下非常有用。 It is less helpful here because the status
resolved
is used by both SingleRecipeState
and RecipesState
, so you still need additional checking.它在这里不太有用,因为已
resolved
的状态由SingleRecipeState
和RecipesState
,因此您仍然需要额外检查。 That's why I've put this option last.这就是为什么我把这个选项放在最后。
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
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.