Как передать компоненту только один тип хранилища (когда хранилище является типом объединения)

#reactjs #typescript #redux #type-safety

#reactjs #typescript #сокращение #безопасность типов

Вопрос:

Я использую Typescript с React / Redux.

Посетитель сайта может находиться в одном из двух состояний, LoggedIn или LoggedOut . Я соответствующим образом структурировал свое состояние:

 interface LoggedIn {
    token: string,
    user: Data.User
}

interface LoggedOut {
    isLoading: boolean,
    lastAttempFailed: boolean
}

type Store = LoggedIn | LoggedOut
  

Исходя из Haskell amp; Elm, это кажется естественным. Теоретически, для реализаций было бы невозможно использовать undefined or null data, потому что они могли бы получить доступ только к состоянию, относящемуся к компонентам (т. Е. После входа пользователя компонент не может получить доступ LoggedOut.isLoading ).).

Как я могу интегрировать это с mapStateToProps ? У меня есть Provider компонент, поставляющий мое хранилище. Я хочу, чтобы определенные компоненты принимали только экземпляр LoggedIn или LoggedOut , а не все хранилище.

В идеале, это должно быть проверено на тип и передано родительским компонентом, например:

 class PrivateRouteComponent extends React.Component<OwnProps amp; ConnectedState, any> {
    render() {
        const { store, component, ...props } = this.props;
        const Component = component;
        switch (store.type) {
            case "LOGGED_IN":
                return <Route render={() => <Component store={store as Store.LoggedIn} {...props}/>}/>;
            case "LOGGED_OUT":
                return <Redirect to="/login"/>;
        }
    }
}
  

Но это халтурно, и передавать хранилище через props кажется неидиоматичным. (В стороне: это также очень затрудняет использование параметров маршрута в react-router ).

Есть ли хорошее, типобезопасное решение этой проблемы? Приветствия

Комментарии:

1. Можете ли вы объяснить, что, по вашему мнению, является хакерским в вашем решении? Это то, что вы приводите store к LoggedIn ? Этого не должно произойти, если вы правильно настроили типы для LoggedIn и LoggedOut . Кроме того, вы говорите, что не хотите передавать все хранилище целиком — но все хранилище — это полный объект для LoggedIn / LoggedOut , так что это отчасти неизбежно. Не могли бы вы пояснить, что вы имеете в виду?

2. В принципе, в Elm / Haskell я могу гарантировать, что мое основное состояние будет в определенной конфигурации, что устраняет необходимость в большом количестве «если состояние равно x, то y, иначе z». Я пытаюсь реализовать те же гарантии в Typescript, чтобы избежать постоянных инструкций switch.

3. TypeScript может гарантировать, что хранилище допустимо , но это не избавляет от каких-либо условных обозначений в вашем коде, поскольку вы захотите обрабатывать их по-разному в зависимости от их состояния.

4. Это то, что я пытаюсь обойти. В Elm я могу сопоставлять шаблоны для типа объединения и соответствующим образом отображать разные маршруты. Затем компоненту передаются определенные типы (например, LoggedIn или LoggedOut ), а не все объединение. Как я могу сделать это аккуратно в Typescript, не передавая хранилище через props (что многие люди не одобряют)?. Возможно, мне нужно что-то сделать с Context ? Я не использовал его раньше.

Ответ №1:

Следующее должно проверять правильность ввода. К сожалению, я считаю, что определения типов этой react-redux утилиты connect должным образом не поддерживают типы объединения, отображаемые в mapStateToProps .

Это все еще имеет условный рендеринг, но я не вижу, чем это отличается от использования сопоставления с образцом в Elm.

 /** represents the store when logged out */
interface LoggedOutStore {
  type: 'LOGGED_OUT';
  login: LoggedOut;
}

/** represents the store when logged in */
interface LoggedInStore {
  type: 'LOGGED_IN';
  login: LoggedIn;
}

type LogState = LoggedInStore | LoggedOutStore; // the part of the store with login state
type AppState = LogState amp; OtherStateFields; // the actual redux store

// dummy components w/ typing
const LoggedInComponent = ({ login }: { login: LoggedIn }) => <></>;
const LoggedOutComponent = ({ logout }: { logout: LoggedOut }) => <></>;

function LogStateComponent(props: LogState) {
  switch (props.type) {
    case 'LOGGED_IN':
      return <LoggedInComponent login={props.login} />;
    case 'LOGGED_OUT':
      return <LoggedOutComponent logout={props.login} />;
  }
}
// Pick<LogState, never> === {}
type ConnectedLogStateComponentType = ConnectedComponentClass<
  typeof LogStateComponent,
  Pick<LogState, never>>;

// this should work in theory; but unfortunately, it needs casting
const ConnectedLogStateComponent = connect((s: AppState): object => s)
  (LogStateComponent) as any as ConnectedLogStateComponentType;

const initState: AppState = {
  login: { isLoading: false, lastAttemptFailed: false },
  type: 'LOGGED_OUT',
};

export const App = () => <Provider store={createStore((s = initState) => s)}>
  <ConnectedLogStateComponent />
</Provider>;