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