Я хочу иметь эту утилиту, которая имеет общий тип объекта и принимает ключ, принадлежащий этому типу, и свойство, связанное с ним, следующим образом:
export type StateBuilder = <StateSchema, Keys extends keyof StateSchema>(
key: Keys,
data: StateSchema[Keys]
) => StateSchema;
Проблема в том, что я не могу передать тип StateSchema. Всегда возвращает ошибку
Type 'StateBuilder' is not generic. ts(2315)
Это должно использоваться внутри функции редуктора, возвращаемой генератором хранилища. Основная идея состоит в том, чтобы передать этот тип функции функции редуктора с помощью генератора хранилища.
import React, { ReactElement, ReactNode, createContext, useContext, useReducer } from 'react';
export type ReducerAction<Type = string> = {
type: Type;
};
export type ReducerActionWithPayload<Type = string, Payload = unknown> = {
type: Type;
payload: Payload;
};
export type StateBuilder = <StateSchema, Keys extends keyof StateSchema>(
key: Keys,
data: StateSchema[Keys]
) => StateSchema;
export const generateStore = <Actions extends ReducerAction | ReducerActionWithPayload, State>(
defaultValue: State,
reducer: <Key extends keyof State, Property extends State[Key]>(
state: State,
action: Actions,
stateBuilder: (key: Key, data: Property) => State
) => State
): {
Provider: (props: { children: ReactNode }) => ReactElement;
dispatcher: (action: Actions) => void;
useStore: () => State;
} => {
const store = createContext(defaultValue);
const { Provider } = store;
let dispatch: React.Dispatch<Actions>;
const ProviderElm = (props: { children: ReactNode }): ReactElement => {
const { children } = props;
const [state, dispatcher] = useReducer(
(state, action) =>
reducer(state, action, (key, data) => ({
...state,
[key]: data,
})),
defaultValue
);
dispatch = dispatcher;
return <Provider value = {state}>{children}</Provider>;
};
return {
Provider: ProviderElm,
dispatcher: (action: Actions) => dispatch && dispatch(action),
useStore: () => useContext(store),
};
};
const DefaultStore = {
token: null as Nullable<string>,
isAuthenticated: false,
};
type ActionTypes = |
ReducerAction<'AUTHENTICATION'> |
ReducerActionWithPayload<'SET_TOKEN', {token: string}>;
const { Provider: FormProvider, dispatcher: formDispatcher, useStore: useFormStore } = generateStore<
ActionTypes,
typeof DefaultStore
>(DefaultStore, (state = DefaultStore, action: ActionTypes, stateBuilder) => {
switch (action?.type) {
case 'AUTHENTICATION': {
return stateBuilder('isAuthenticated', true);
}
case 'SET_TOKEN': {
return stateBuilder('token', action.payload);
}
default:
return state;
}
});
export { FormProvider, formDispatcher, useFormStore };
В настоящее время возвращает ошибку
Argument of type '"isAuthenticated"' is not assignable to parameter of type 'Key'.
'"isAuthenticated"' is assignable to the constraint of type 'Key', but 'Key' could be instantiated with a different subtype of constraint '"isAuthenticated" | "token"'.ts(2345)





Я думаю, что вы ищете что-то вроде этого:
export type StateBuilder<StateSchema, Keys extends keyof StateSchema> = (
key: Keys,
data: StateSchema[Keys]
) => StateSchema;
Здесь сам тип StateBuilder<T, K> является универсальным, и вам нужно указать оба его аргумента типа, чтобы использовать его. То, как вы это объявляли, StateBuilder является псевдонимом универсального типа функции, переменные типа которого должны всегда выводиться и не могут быть указаны из самого имени StateBuilder.
Более полезный ответ может быть дан, если вы представите пример того, как вы хотите использовать тип и что вы от него ожидаете. Может быть, вы хотите указать только тип объекта и позволить TS сделать вывод о типе ключа?
Возможно, я неправильно понял, что вы пытаетесь сделать, но похоже, что вы пытаетесь сделать функцию, которая принимает состояние и обновляет в нем значения. В этом случае вам все равно нужно указать исходное или начальное состояние. Что-то вроде этого может быть тем, что вы ищете.
Вы можете просто создать универсальную функцию и не беспокоиться о создании универсального типа, а затем использовать его для создания функции. Вы можете использовать эту служебную функцию в любом магазине.
const stateBuilder = <TState, TKey extends keyof TState>(state: TState, key: TKey, value: TState[TKey]) => ({ ...state, [key]: value });
let defaultValue = {
foo: "hello",
bar: 10
}
defaultValue = stateBuilder(defaultValue, "bar", 20);
console.info(defaultValue);
ОБНОВЛЯТЬ
Это должно быть то, что вы ищете. Основная часть — тип StateBuilder. Требовались дженерики с обеих сторон. С одной стороны, чтобы сообщить вам, с каким состоянием он работает, а с другой стороны, для ключа/значения, поскольку они основаны на том, что вы передаете функции.
import React, { ReactElement, ReactNode, createContext, useContext, useReducer } from 'react';
export type ReducerAction<Type = string> = {
type: Type;
};
export type ReducerActionWithPayload<Type = string, Payload = unknown> = {
type: Type;
payload: Payload;
};
type StateBuilder<State> = <Key extends keyof State, Value extends State[Key]>(key: Key, value: Value) => State
export const generateStore = <Actions extends ReducerAction | ReducerActionWithPayload, State>(
defaultValue: State,
reducer: <Key extends keyof State, Value extends State[Key]>(
state: State,
action: Actions,
stateBuilder: StateBuilder<State>
) => State
): {
Provider: (props: { children: ReactNode }) => ReactElement;
dispatcher: (action: Actions) => void;
useStore: () => State;
} => {
const store = createContext(defaultValue);
const { Provider } = store;
let dispatch: React.Dispatch<Actions>;
const ProviderElm = (props: { children: ReactNode }): ReactElement => {
const { children } = props;
const [state, dispatcher] = useReducer(
(state: State, action: Actions) =>
reducer(state, action, (key, data) => ({
...state,
[key]: data,
})),
defaultValue
);
dispatch = dispatcher;
return <Provider value = {state}>{children}</Provider>;
};
return {
Provider: ProviderElm,
dispatcher: (action: Actions) => dispatch && dispatch(action),
useStore: () => useContext(store),
};
};
export type DefaultStore = {
token?: string;
isAuthenticated?: boolean;
}
const defaultStore: DefaultStore = {
token: "",
isAuthenticated: false,
};
type ActionTypes = |
ReducerAction<'AUTHENTICATION'> |
ReducerActionWithPayload<'SET_TOKEN', { token: string }>;
const { Provider: FormProvider, dispatcher: formDispatcher, useStore: useFormStore } = generateStore<
ActionTypes,
DefaultStore
>(defaultStore, (state = defaultStore, action: ActionTypes, stateBuilder) => {
switch (action?.type) {
case 'AUTHENTICATION': {
return stateBuilder('isAuthenticated', true); //typechecked
}
case 'SET_TOKEN': {
return stateBuilder('token', action.payload.token); //typechecked
}
default:
return state;
}
});
export { FormProvider, formDispatcher, useFormStore };
Хорошо, похоже, что вы в основном делаете то же, что и я при передаче функции в более высокую область, поэтому состояние уже есть. Позвольте мне обновить мой ответ.
Я не понял вашего вопроса. Я создаю stateBuilder при вызове функции useReducer React. Также спасибо за помощь :-)
Извините за задержку, я внес некоторые изменения в ваш пример, чтобы проверка типа работала над вашими действиями.
Извините, не уверен, почему это не работает. Мой первый тип функции отлично проверяет тип свойства, и единственное, что отличается, — это параметр состояния.
Хорошо, это заняло минуту, но, думаю, на этот раз я понял.
Но в моем случае мне не нужно передавать состояние, поскольку оно уже имеет доступ к схеме состояния в верхней области. А также, при использовании построителя состояний я хочу иметь intelisense для ключа и значения на основе того, что я передаю. Например, если я позвоню
stateBuilder('bar', 'not a number'), он должен сообщить мне о несоответствии