javascript - 使用 React 的 useContext 理解 Typescript Record 类型
问题描述
我在为 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)
}
我想要做的是拥有一个可以使用减速器和初始状态调用的函数,以生成可以在应用程序中的任何位置使用的上下文提供程序。在这种情况下,我试图生成一个AuthContext
. 此上下文将采用如下所示的减速器:
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>
usingunknown
也可以,但现在打字稿抱怨说Object is of type 'unknown'.
,当做: 时boundActions[key] = actions[key](dispatch)
,特别是 on 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],
}
这正是我想要的,因为我希望能够在我的代码中的任何位置调用这些函数,这反过来又会调用特定减速器的调度函数来改变特定上下文的状态。
现在,我对 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>
)
}
这实际上有效,但它不是类型安全的。
谢谢你。
解决方案
如果您正在处理接受动作创建者的不同参数的动作,那么您希望根据这些特定动作的映射而不是模糊的通用类型来定义您的类型。
由于您在这里只有两个动作创建者,他们都不接受任何参数(除了调度),那么它们都可以用以下类型来描述:
(dispatch: React.Dispatch<Action>) => () => void
所以你会有
type Actions = Record<string, (dispatch: React.Dispatch<Action>) => () => void>
我仍然不喜欢这种类型,因为它具有string
作为键类型而不是您的特定操作功能login
和logout
. 但是现在我们已经更好地输入了它,Typescript 可以开始按预期工作并给我们提供有用的错误。
您在函数中创建的boundActions
不应与actions
我们作为参数传递的类型相同。actions
是接受Dispatch
和返回函数的函数() => void
。boundActions
只是返回的函数() => void
。
因此,我们开始看到您的设计存在一些问题。将未绑定的操作用作上下文的默认值(或通过类型检查)是没有意义的,因为它们本质上与其绑定的版本不同。
当您仅将Function
其用作类型时,这些错误就被隐藏了,这正是它危险的原因!
您可以为您创建实体类型,AuthContext
而无需做任何“花哨”的事情。但是您想创建一个支持创建多个不同上下文的工厂。您可以使用很多模糊类型,例如Record
andany
并且不会出现任何打字稿错误,但是当您使用上下文时也不会获得太多有用的信息。上有哪些属性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>
)
}
推荐阅读
- python - 最大和增加子序列(但输出是一个列表)
- python - 将用户输入设置为信号量变量的问题
- python - RTX 3080,Ubuntu 18.04,cuda 11.1,gcc 9.1.0,无法安装 pytorch 1.7.1
- python - 如何防止多个线程从队列中获取相同的任务
- python - 从 while 循环返回 pandas 数据帧
- c++ - C++ strcat_s 字符串不是空终止的
- pascal - Turbo Pascal 结果不可见
- python - 使用 MatPlotLib 将多个条形图绘制成图表
- node.js - 角色搜索命令 Discord.js
- c - 如何在C中分配复变量的实部和虚部