使用React的useContext理解Typescript记录类型

pn9klfpd  于 2023-03-31  发布在  TypeScript
关注(0)|答案(2)|浏览(124)

我在为React上下文开发的生成器函数中遇到了一些问题,无法理解记录类型。
下面是我的generator函数:

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和初始状态来生成一个Context提供程序,我可以在应用程序中的任何地方使用。在这个例子中,我试图生成一个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<string, Function>
使用unknown也可以工作,但是现在typescript在执行以下操作时会抱怨Object is of type 'unknown'.boundActions[key] = actions[key](dispatch),特别是在actions[key]上,因为我把它作为一个函数调用。
action是接受函数(dispatch)作为参数并返回State对象的函数。
下面是我的actions声明来进一步说明:

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

如果我在generator中控制台日志boundActions,我会得到如下内容:

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

这正是我想要的,因为我希望能够在代码中的任何地方调用这些函数,而这反过来又会调用特定reducer的dispatch函数来更改特定上下文的状态。
现在,我对Typescript相当陌生,但我的直觉告诉我Actions类型的声明应该是这样的:
export type Actions = Record<string, (dispatch:Function) => State>
这样做的问题是: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<string, (dispatch(arg:Action)) => State>
但这也不起作用,因为它不是有效的类型脚本代码。
我知道这种情况有点不必要的复杂,但我只是想更好地理解Record在这种特定情况下的使用,其中记录具有string键和Function值。
在一天结束的时候,我只想做这样的事情:

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

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

它实际上可以工作,但不是类型安全的。
谢谢大家。

6gpjuf90

6gpjuf901#

如果你正在处理接受不同参数的动作,那么你需要基于这些特定动作的Map来定义你的类型,而不是基于一个模糊的一般化类型。
由于这里只有两个action creator,它们都不接受任何参数(除了dispatch),因此它们都可以用以下类型描述:

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

所以你会

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

我仍然不喜欢这个类型,因为它的键类型是string,而不是你的特定操作函数loginlogout。但是现在我们已经输入了更好的类型,Typescript可以开始按预期工作,并给我们提供有用的错误。
在函数中创建的boundActions不应该与我们作为参数传递的actions具有相同的类型。actions是接受Dispatch并返回函数() => void的函数。boundActions只是返回函数() => void
所以我们开始看到你的设计有一些问题。使用未绑定的动作作为上下文的默认值是没有意义的(或者通过类型检查),因为这些动作与它们的绑定版本本质上是不同的。
当您只是将Function用作类型时,这些错误被隐藏了起来,这正是它危险的原因!
你可以为你的AuthContext创建实体类型,而不需要做任何“花哨”的事情。但是你想创建一个支持创建多个不同上下文的工厂。你可以使用很多模糊类型,比如Recordany,而且你不会有任何类型错误。但是当你使用上下文时,你也不会得到很多有用的信息。state上有什么属性?我可以调用什么操作?如果你想创建一个工厂,它知道它正在创建的特定上下文,那么你需要使用更高级的Typescript特性,比如泛型和Map类型。
通用设置

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

zazmityj

zazmityj2#

我认为这个例子有助于理解TS中的Record类型。

type AppProps = {
  /** a dictionary object with any number of properties of the same type */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; // equivalent to dict1
}

相关问题