【问题标题】:React/TypeScript: Union Types in state of Context APIReact/TypeScript:上下文 API 状态下的联合类型
【发布时间】:2020-11-12 05:36:10
【问题描述】:

当我在其他组件中使用状态时,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 }

【问题讨论】:

  • 由于上下文值是多个状态类型的并集,因此在访问仅存在于成功类型上的属性之前,您必须检查您的值是否为成功类型。
  • 我会稍微不同地定义联合,以便您可以使用 status 的值来计算其余部分。继续阅读“受歧视的工会”。我可以留下一个答案。
  • @LindaPaiste 好的,感谢您的回答。我会阅读它,但非常感谢您的回答:D!再次感谢!
  • @LindaPaiste 你的意思是这样的typescriptlang.org/docs/handbook/…,prettier 似乎不喜欢第一个|,而且它似乎仍然不起作用:c
  • 是的,文档页面正是我的意思。尽管在更仔细地查看了您的代码之后,可区分联合在这里并不完美,因为您有两种不同类型的 success 响应。我刚刚给你留了一个冗长的答案。

标签: reactjs typescript


【解决方案1】:

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

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

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

保护属性

在不更改类型定义的情况下,您可以做的最简单的事情是使用type guard 来查看属性是否存在。根据你的联合,打字稿知道如果有一个属性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 字符串文字(嗯,大部分情况下),但我们没有利用这一事实。 Discriminating unions 是一种基于 status 等字符串属性的值来缩小对象类型的方法。

您已经在使用 Action 联合类型执行此操作。当你基于action.typeswitch 时,它知道action.payload 的正确类型。

这在某些情况下非常有用。这里的帮助不大,因为SingleRecipeStateSingleRecipeStateRecipesState 都使用状态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
  }
}

Playground Link

【讨论】:

  • 谢谢!现在我使用三元运算符检查“食谱”是否处于状态,如果是,那么我只需将其映射出来并返回食谱:D.
猜你喜欢
  • 2021-07-20
  • 1970-01-01
  • 2020-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多