简体   繁体   English

React/TypeScript:Context API 状态下的联合类型

[英]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:基本上有两种方法可以处理这种类型的事情:

  1. 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

  2. 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

Guarding Properties保护属性

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;
    }
}

Declaring Optional Properties声明可选属性

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)

Discriminating Unions歧视工会

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的状态由SingleRecipeStateRecipesState ,因此您仍然需要额外检查。 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
  }
}

Playground Link游乐场链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM