Typescript не может принудительно передать дженерик

Я хочу иметь эту утилиту, которая имеет общий тип объекта и принимает ключ, принадлежащий этому типу, и свойство, связанное с ним, следующим образом:

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)
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
0
213
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, что вы ищете что-то вроде этого:

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 };

Но в моем случае мне не нужно передавать состояние, поскольку оно уже имеет доступ к схеме состояния в верхней области. А также, при использовании построителя состояний я хочу иметь intelisense для ключа и значения на основе того, что я передаю. Например, если я позвоню stateBuilder('bar', 'not a number'), он должен сообщить мне о несоответствии

Anthony White 14.12.2020 18:06

Хорошо, похоже, что вы в основном делаете то же, что и я при передаче функции в более высокую область, поэтому состояние уже есть. Позвольте мне обновить мой ответ.

Todd Skelton 14.12.2020 18:10

Я не понял вашего вопроса. Я создаю stateBuilder при вызове функции useReducer React. Также спасибо за помощь :-)

Anthony White 14.12.2020 18:14

Извините за задержку, я внес некоторые изменения в ваш пример, чтобы проверка типа работала над вашими действиями.

Todd Skelton 15.12.2020 01:25

Извините, не уверен, почему это не работает. Мой первый тип функции отлично проверяет тип свойства, и единственное, что отличается, — это параметр состояния.

Todd Skelton 15.12.2020 01:44

Хорошо, это заняло минуту, но, думаю, на этот раз я понял.

Todd Skelton 15.12.2020 02:07

Другие вопросы по теме