【问题标题】:Understanding Typescript Record type with React's useContext使用 React 的 useContext 理解 Typescript Record 类型
【发布时间】:2020-12-08 11:58:17
【问题描述】:

我在为 React 上下文开发的生成器函数中理解记录类型时遇到了一些问题。

这是我的生成器函数:

import * as React from 'react'

export type State = Record<string, unknown>
export type Action = { type: string; payload?: unknown }

// eslint-disable-next-line @typescript-eslint/ban-types
export type Actions = Record<string, Function>


export type AppContext = { state: State; actions: Actions }
export type Reducer = (state: State, action: Action) => State

type ProviderProps = Record<string, unknown>
type FullContext = {
    Context: React.Context<AppContext>
    Provider: React.FC<ProviderProps>
}

/**
 * Automates context creation
 *
 * @param {Reducer} reducer
 * @param {Actions} actions
 * @param {State} initialState
 * @returns {Contex, Provider}
 */
export default (
    reducer: Reducer,
    actions: Actions,
    initialState: State,
    init: () => State = (): State => initialState,
): FullContext => {
    const Context = React.createContext<AppContext>({
        state: { ...initialState },
        actions,
    })

    const Provider = ({ children }: { children?: React.ReactNode }) => {
        const [state, dispatch] = React.useReducer(reducer, initialState, init)

        const boundActions: Actions = {}
        for (const key in actions) {
            boundActions[key] = actions[key](dispatch)
        }

        return (
            <Context.Provider value={{ state, actions: { ...boundActions } }}>
                {children}
            </Context.Provider>
        )
    }

    return {
        Context,
        Provider,
    }
}

问题的核心在这里:

        const boundActions: Actions = {}
        for (const key in actions) {
            boundActions[key] = actions[key](dispatch)
        }

我正在尝试做的是拥有一个可以使用 reducer 和初始状态调用的函数,以生成可以在应用程序中的任何位置使用的上下文提供程序。在这种情况下,我试图生成一个AuthContext。此上下文将采用如下所示的 reducer:

const authReducer: Reducer = (state: State, action: Action): State => {
    switch (action.type) {
        case 'login':
            return { ...state, isLogged: true, avoidLogin: false }
        case 'logout':
            return { ...state, isLogged: false, avoidLogin: true }
        default:
            return { ...state }
    }
}

相当准系统,但它基本上会改变对象中的一些布尔值。

现在,这段代码确实有效,但让我烦恼的是在 Actions 类型的声明中使用了 Function 类型。

export type Actions = Record&lt;string, Function&gt;

使用unknown 也可以,但现在打字稿抱怨Object is of type 'unknown'. 在执行以下操作时:boundActions[key] = actions[key](dispatch),特别是在actions[key],因为我将它作为函数调用。

动作是接受一个函数(调度)作为参数并返回一个State对象的函数。

这是我的行动声明,以进一步说明:

const actions: Actions = {
    login: (dispatch: (action: Action) => void) => () => {
        dispatch({ type: 'login' })
    },
    logout: (dispatch: (action: Action) => void) => () => {
        dispatch({ type: 'logout' })
    },
}

如果我在生成器中控制台日志boundActions,我会得到类似:

Object {
  "login": [Function anonymous],
  "logout": [Function anonymous],
}

这正是我想要的,因为我希望能够在我的代码中的任何位置调用这些函数,而这又会调用特定 reducer 的调度函数来更改特定上下文的状态。

现在,我对 Typescript 还很陌生,但我的直觉告诉我 Actions 类型的声明应该是这样的:

export type Actions = Record&lt;string, (dispatch:Function) =&gt; State&gt;

这个问题是: 1- 它不起作用,因为现在boundActions 说:

Type 'Record<string, unknown>' is not assignable to type '(dispatch: Function) => Record<string, unknown>'.
  Type 'Record<string, unknown>' provides no match for the signature '(dispatch: Function): Record<string, unknown>'.

2- 我仍在使用非类型安全的 Function 类型。

现在,dispatch 将一个动作作为参数export type Action = { type: string; payload?: unknown }

所以我的猜测是export type Actions = Record&lt;string, (dispatch(arg:Action)) =&gt; State&gt;

但这也不起作用,因为它不是有效的打字稿代码。

我知道这种情况有点复杂,但我只是想更好地了解 Record 在这种特定情况下的使用,其中记录具有 string 键和 Function 值。

在一天结束的时候,我只想做这样的事情:

export default function Login(): JSX.Element {
    const {
        actions: { login },
    } = React.useContext(AuthContext)

    return (
        <View>
            <Button text='Entrar' onPress={() => login()} />
        </View>
    )
}

这确实有效,但它不是类型安全的。

谢谢。

【问题讨论】:

    标签: javascript reactjs typescript react-context


    【解决方案1】:

    如果您要处理的动作接受动作创建者的不同参数,那么您希望根据这些特定动作的映射而不是模糊的概括类型来定义类型。

    由于这里只有两个动作创建者,他们都没有接受任何参数(除了调度),那么它们都可以用以下类型来描述:

    (dispatch: React.Dispatch<Action>) => () => void
    

    所以你会有

    type Actions = Record<string, (dispatch: React.Dispatch<Action>) => () => void>
    

    我还是不喜欢这种类型,因为它有string 作为键类型,而不是你的特定操作函数loginlogout。但是现在我们已经更好地输入了它,Typescript 可以开始按预期工作并给我们提供有用的错误。

    您在函数中创建的boundActions 不应与我们作为参数传递的actions 具有相同的类型。 actions 是接受 Dispatch 并返回函数 () =&gt; void 的函数。 boundActions 只是返回的函数() =&gt; void

    所以我们开始发现您的设计存在一些问题。将未绑定的操作用作上下文的默认值(或通过类型检查)是没有意义的,因为它们本质上与其绑定的版本不同。

    当您仅使用 Function 作为类型时,这些错误被隐藏了,这正是它危险的原因!

    您可以为您的AuthContext 创建实体类型,而无需做任何“花哨”的事情。但是您想创建一个支持创建多个不同上下文的工厂。你可以使用很多模糊的类型,比如Recordany,你不会有任何打字稿错误,但是当你去使用上下文时你也不会得到太多有用的信息。 state 上有哪些属性?我可以调用哪些操作?如果您想创建一个了解它正在创建的特定上下文的工厂,那么您需要使用更高级的 Typescript 功能,例如泛型和映射类型。

    通用设置

    import React, { Reducer, Dispatch as _Dispatch } from 'react';
    
    // Very generalized action type
    export type Action = { type: string; payload?: any }
    
    // We are not using specific action types.  So we override the React Dispatch type with one that's not generic
    export type Dispatch = _Dispatch<Action>
    
    // 
    
    // Map from action functions of dispatch to their bound versions
    type BoundActions<ActionMap> = {
        [K in keyof ActionMap]: ActionMap[K] extends ((dispatch: Dispatch) => infer A) ? A : never
    }
    
    // The type for the context depends on the State and the action creators
    export type AppContext<State, ActionMap> = {
        state: State;
        actions: BoundActions<ActionMap>;
    }
    
    type FullContext<State, ActionMap> = {
        Context: React.Context<AppContext<State, ActionMap>>;
        Provider: React.FC; // Provider does not take any props -- just children
    }
    
    const createAuthContext = <
        // type for the state
        State,
        // type for the action map
        ActionMap extends Record<string, (dispatch: Dispatch) => (...args: any[]) => void>>(
            reducer: Reducer<State, Action>,
            actions: ActionMap,
            initialState: State,
            init: () => State = (): State => initialState,
    ): FullContext<State, ActionMap> => {
        const Context = React.createContext<AppContext<State, ActionMap>>({
            state: { ...initialState },
            actions: Object.fromEntries(
                Object.keys(actions).map(key => (
                    [key, () => console.error("cannot call action outside of a context provider")]
                )
                )) as BoundActions<ActionMap>,
        })
    
        const Provider = ({ children }: { children?: React.ReactNode }) => {
            const [state, dispatch] = React.useReducer(reducer, initialState, init)
    
            // this is not the most elegant way to handle object mapping, but it always requires some assertion
            // there is already a bindActionCreators function in Redux that does this.
            const boundActions = {} as Record<string, any>
            for (const key in actions) {
                boundActions[key] = actions[key](dispatch)
            }
    
            return (
                <Context.Provider value={{ state, actions: boundActions as BoundActions<ActionMap> }}>
                    {children}
                </Context.Provider>
            )
        }
    
        return {
            Context,
            Provider,
        }
    }
    

    具体实现

    export type AuthState = {
        isLogged: boolean;
        avoidLogin: boolean;
    }
    
    const authReducer = (state: AuthState, action: Action): AuthState => {
        switch (action.type) {
            case 'login':
                return { ...state, isLogged: true, avoidLogin: false }
            case 'logout':
                return { ...state, isLogged: false, avoidLogin: true }
            default:
                return { ...state }
        }
    }
    
    // no type = let it be inferred
    const actions = {
        login: (dispatch: Dispatch) => () => {
            dispatch({ type: 'login' })
        },
        logout: (dispatch: Dispatch) => () => {
            dispatch({ type: 'logout' })
        },
    }
    
    const initialAuthState: AuthState = {
        isLogged: false,
        avoidLogin: true
    }
    
    const { Context: AuthContext, Provider } = createAuthContext(authReducer, actions, initialAuthState);
    
    export function Login(): JSX.Element {
        // this is where the magic happens!
        const {
            // can only destructure known actions, will get errors on typos
            actions: { login },
            state
        } = React.useContext(AuthContext)
    
        // we now know all the properties of the state and their types
        const { isLogged } = state;
    
        return (
            <View>
                <Button text='Entrar' onPress={() => login()} />
            </View>
        )
    }
    

    Typescript Playground Link

    【讨论】:

      猜你喜欢
      • 2021-09-11
      • 1970-01-01
      • 2020-09-29
      • 2022-12-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多