Definitelytyped: `connect` в react-redux нельзя использовать в качестве декоратора: тип" не может быть назначен типу 'void' "

Созданный на 4 июл. 2016  ·  32Комментарии  ·  Источник: DefinitelyTyped/DefinitelyTyped

Попытка использовать connect в Type 'foo' is not assignable to type 'void' . Проблема с этими типами, похоже, заключается в том, что TypeScript не позволяет ClassDecorator s изменять сигнатуру того, что они украшают , но это текущая реализация типизации response-redux и делается целенаправленно, поскольку response-redux возвращает другой экземпляр с другой подписью для реквизита .

Использование connect в качестве функции работает должным образом; только использование в качестве декоратора вызывает проблемы с типом.

Предложения приветствуются, но любые предлагаемые решения должны быть строгим улучшением того, что типизация может выражать в настоящее время, то есть принесение в жертву текущей правильности типизации использования как функции неприемлемо (отчасти потому, что это было бы серьезная регрессия и отчасти потому, что декораторы считаются «экспериментальными» и, следовательно, я утверждаю, вторичными по сравнению со стандартным использованием функций).

Я не знаю, возможно ли полное решение проблемы с типизацией декоратора, в то время как https://github.com/Microsoft/TypeScript/issues/4881 выдающийся. Тем не менее, в этом случае есть постепенные улучшения, такие как (первый проход) вывод любого вида React.ComponentClass чтобы код хотя бы компилировался, затем (второй проход) вывод компонента, который принимает пересечение всех props (как собственные, так и связанные), хотя это было бы слишком мягко, поэтому тип кода проверяет хотя бы некоторые из props.

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

Вместо того:

@connect(mapStateToProps, mapDispatchToProps)
class MyComponent extends React.Component<...> { ... }

использовать (что-то вроде):

class MyComponentImpl extends React.Component<...> { ... }
const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentImpl);

Обратите внимание, что https://github.com/DefinitiTyped/DefinitiTyped/issues/8787 вступает в игру и иногда ассоциируется с этой проблемой. Вам также может понадобиться подсказка типа или приведение, чтобы убедиться, что выходной компонент имеет правильный тип.

Изменить: https://github.com/Microsoft/TypeScript/pull/12114 может помочь в этом случае: декоратор потенциально может быть написан для создания необязательной версии реквизита для сгенерированного компонента, что не совсем идеально но по-прежнему позволяет вашей реализации более настойчиво относиться к аннулированию пропов.

Самый полезный комментарий

Чтобы использовать @connect напрямую (без использования специальных декораторов), я использую следующий обходной путь:

@(connect(mapStateToProps, mapDispatchToProps) as any)
class MyComponent extends React.Component<...> {...}

Но я полностью согласен с @seansfkelley в том, что любой набор текста на самом деле не то, что мы хотим иметь ...

Все 32 Комментарий

На днях у меня были похожие проблемы, и я отказался от функции подключения. Типы смотрели и использовали старые типы реакции. Я могу попытаться исправить это и отправить PR?

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

Возможно, сопоставленные типы могут помочь вам в этом, так что, по крайней мере, у вас будет _some_ полезная информация о типах в выходных данных?

ЙОЛО

export function myConnect(mapStateToProps, mapDispatchToProps) {
    return function (target) {
        return <any>connect(mapStateToProps, mapDispatchToProps)(target);
    }
}

Я не думаю, что есть особый смысл в использовании yolo-typing, потому что обходные пути, перечисленные здесь и в # 8787, в частности, об использовании подсказок для вызова функции, не так уж и плохи (особенно с шаблонами IDE для классов компонентов) а иметь компонент с любым типом - это грустно. :( Любой набор текста подключается только для того, чтобы использовать его в качестве декоратора вместо вызова функции, действительно ставит телегу впереди лошади.

Чтобы использовать @connect напрямую (без использования специальных декораторов), я использую следующий обходной путь:

@(connect(mapStateToProps, mapDispatchToProps) as any)
class MyComponent extends React.Component<...> {...}

Но я полностью согласен с @seansfkelley в том, что любой набор текста на самом деле не то, что мы хотим иметь ...

Несмотря на то, что синтаксис декоратора - это круто, я по-прежнему считаю, что использование HOC, например подключения, стандартным способом, является лучшей практикой.

1) Он изменяет тип класса, который противоречит спецификации декоратора.
2) Connect - это вообще не декоратор, это HOC.
3) Это нарушает переносимость кода, поскольку имеет другой синтаксис для компонентов без состояния и без состояния.

Поэтому я хотел бы сказать, пожалуйста, не давайте определение, которое позволяет разработчикам использовать его неправильно :)

@npirotte Согласен. Вы абсолютно правы, использование HOC в качестве декораторов нарушает спецификацию декораторов. Это уже не базовый класс, а совершенно другой.

У меня точно такие же проблемы с пользовательскими декораторами, и я смотрю на состояние их поддержки в TS (они все еще находятся за флагом, и AFAICS не планирует включать их по умолчанию) Я собираюсь прекратить поддержку их. Более того, декораторы все еще находятся в черновике w3c.

Лучший способ обернуть компонент в новый - использовать для него функцию более высокого порядка вместо того, чтобы пытаться _decorate_ существующий класс.

У меня такая же ошибка, если я ее импортирую. Однако для меня работает следующее:

const { connect } = require('react-redux');

@connect(mapStateToProps, mapDispatchToProps)
class MyComponent extends React.Component<...> { ... }

@TriStarGod, это зависит от того, как вы набрали require . Похоже, что в этом случае вы получите connect набранное как any .

Другое решение / обходной путь.

Поскольку у меня уже есть собственное состояние приложения, мне нужна очень короткая функция в виде:

export interface PageProps {
    routing: RouterState;
}

export interface PageDispatch {
    navigate: () => void
}

@connect<PageProps, PageDispatch>(
    state => ({
        routing: state.routing
    }),
    dispatch => ({
        navigate: () => dispatch(push("/"))
    })
)
export class Page extends React.Component<PageProps & PageDispatch> {
...
}

А вот этот обернутый метод подключения:

import { connect } from "react-redux";
import { ApplicationState } from './rootReducer';

interface MapPropsParam<TProps>{
    (state: ApplicationState, ownProps?: TProps): TProps
}

interface MapDispatchParam<TProps, TDispatchProps>{
   (dispatch: Redux.Dispatch<ApplicationState>, ownProps?: TProps): TDispatchProps;
}

export interface ConnectedComponent<TProps> {
    <TComponent extends React.ComponentType<TProps>>(component: TComponent): TComponent;
}

function connectToAppState<TProps, TDispatchProps = {}>(mapProps: MapPropsParam<TProps>, mapDispatch?: MapDispatchParam<TProps, TDispatchProps>) : ConnectedComponent<TProps> {
    return connect<TProps, TDispatchProps, TProps>(mapProps, mapDispatch) as ConnectedComponent<TProps & TDispatchProps>;    
}

export {
    connectToAppState as connect
}

Спасибо за вашу работу. Подписываемся и ждем прогресса по этому вопросу.

@offbeatful, укажите тип другого варианта mapDispatchToProps

Если объект передан, предполагается, что каждая функция внутри него является создателем действия Redux. Объект с такими же именами функций, но с каждым создателем действия, заключенным в вызов диспетчеризации, чтобы их можно было вызывать напрямую, будет объединен в свойства компонента.

другой обходной путь, основанный на комментарии @offbeatful

myConnect.ts

import {
    connect as originalConnect, MapDispatchToPropsParam, MapStateToPropsParam, MergeProps, Options
} from "react-redux";
import * as React from "react";

export interface InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> {
    <TComponent extends React.ComponentType<TInjectedProps & TNeedsProps>>(component: TComponent): TComponent;
}

interface MyConnect {
    <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
        mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps>,
        mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>
    ): InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>;

    <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
        mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps>,
        mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
        mergeProps?: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
        options?: Options<TStateProps, TOwnProps, TMergedProps>
    ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>;

}

export const connect = originalConnect as MyConnect;

Обновлено на основе фрагмента

import { ApplicationState } from "./rootReducer";

import * as React from "react";
import {
    connect as originalConnect, MapDispatchToPropsParam, MapStateToPropsParam, MergeProps, Options
} from "react-redux";

export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <TComponent extends React.ComponentType<TInjectedProps & TNeedsProps>>(component: TComponent) => TComponent;

interface MyConnect {
    <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
        mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, ApplicationState>,
        mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>
    ): InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>;

    <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
        mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, ApplicationState>,
        mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
        mergeProps?: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
        options?: Options<TStateProps, TOwnProps, TMergedProps>
    ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>;

}

export const connect = originalConnect as MyConnect;

Привет, @offbeatful ( @pravdomil ?), Я недавно задал вопрос о переполнении стека, связанный с этой проблемой, и был сделан вывод, что это в настоящее время не поддерживается. В рамках этого вопроса я подготовил репозиторий, чтобы продемонстрировать, что я пробовал. Был сделан вывод, что это в настоящее время не поддерживается, поэтому я был рад увидеть ваш фрагмент кода сегодня и попытался обновить свой репозиторий, чтобы использовать его.

Здесь вы можете увидеть фиксацию с интегрированным фрагментом кода.

Это больше не кричит об ошибках, которые я получал раньше, но я обнаружил, что в моем случае использования, когда мой компонент подключен, но также имеет свои собственные реквизиты, эта новая подпись теперь будет делать так, что даже реквизиты состояния требуются как собственный (TSX) реквизит. cd my-app && yarn start в моем репозитории продемонстрирует, что я имею в виду.

Как вы думаете, это тоже можно решить, или это невозможно?

вы можете использовать эту версию connect

connect(
        mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
        mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
        mergeProps: null | undefined,
        options: Options<State, TStateProps, TOwnProps>
): InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>

если это так, вам просто нужно исправить параметр options чтобы он был Options<any, any, any> таким образом у вас не будет проблемы TStateProps & TDispatchProps потому что это будет TStateProps & any

в двух словах:

const options = { withRef:true } as Options<any, any, any>;
export const Component = connect(mapStateToProps, mapDispatchToProps, null, options)

круто как ты

@TomasHubelbauer Просто попробовал ваш метод, если вы вернетесь к методу подключения по умолчанию redux
как ни странно это будет работать.

Лучше всего убедиться, что вы используете общие типы между тремя:

export type CounterStateProps = {
    count: number;
};

export type CounterDispatchProps = {
    onIncrement: () => void;
};

export type CounterOwnProps = {
    initialCount: number;
};

export type CounterProps = CounterStateProps & CounterDispatchProps & CounterOwnProps;

Затем реализуйте компонент с отслеживанием состояния

export class StatefulCounter extends React.Component<CounterProps, CounterStateProps> {
    timer: number;

    componentDidMount() {
        this.timer = setInterval(this.props.onIncrement, 5000);
    }

    componentWillUnmount() {
        clearInterval(this.timer);
    }

    render() {
      return (
        <StatelessCounter count={this.props.count}/>
      );
    }
}

И, наконец, сделайте класс коннектора таким же, используя сборку redux в connect, а НЕ пользовательский код подключения.

const mapStateToProps =
    (state: RootState, ownProps: CounterOwnProps): CounterStateProps => ({
        count: countersSelectors.getReduxCounter(state) + ownProps.initialCount,
    });

const mapDispatchToProps =
    (dispatch: Dispatch<CounterActionType>): CounterDispatchProps => bindActionCreators({
        onIncrement: CounterActions.increment,
    }, dispatch);

export const ConnectedCounter =
    connect(mapStateToProps, mapDispatchToProps)(StatefulCounter);

@JohnHandley Да, я знаю, что это работает, в моем примере я показал это до того, как попробовал некоторые из предложений, чтобы заставить его работать как декоратор. Я успешно использую вариант без декоратора, но мне бы очень хотелось, чтобы декоратор тоже работал.

Кроме того, я думаю, вы перепутали свои типы, вы используете CounterStateProps (который является возвращаемым типом mapDispatchToProps как компонент CounterProps (что нормально, у него есть состояние Redux props, Redux dispatch props и JSX own props), но также как тип для состояния компонентов.Вместо этого state должен иметь свой собственный тип, который никоим образом не участвует в сигнатуре типа внешнего компонента.

Возможно, я не полностью понял ваше решение, поэтому, если вы можете выполнить эту работу в моем репозитории (где я использую как собственные реквизиты в TSX, так и в реквизитах состояния Redux), не получая сообщения об ошибке, говорящего, что вам нужно указать реквизиты состояния Redux в TSX Тоже было бы здорово!

+1 по этому вопросу

Я обновил фрагмент из @offbeatful и теперь успешно его использую.

connect.ts ( IAppState - это интерфейс состояния редукции)

import React from 'react'
import {
    connect as originalConnect,
    MapDispatchToPropsParam,
    MapStateToPropsParam,
    MergeProps,
    Options,
} from 'react-redux'


export type InferableComponentEnhancerWithProps<IInjectedProps, INeedsProps> =
    <IComponent extends React.ComponentType<IInjectedProps & INeedsProps>>(component: IComponent) => IComponent

export interface IConnect {
    <IStateProps = {}, IDispatchProps = {}, IOwnProps = {}>(
        mapStateToProps?: MapStateToPropsParam<IStateProps, IOwnProps, IAppState>,
        mapDispatchToProps?: MapDispatchToPropsParam<IDispatchProps, IOwnProps>,
    ): InferableComponentEnhancerWithProps<IStateProps & IDispatchProps, IOwnProps>

    <IStateProps = {}, IDispatchProps = {}, IOwnProps = {}, IMergedProps = {}>(
        mapStateToProps?: MapStateToPropsParam<IStateProps, IOwnProps, IAppState>,
        mapDispatchToProps?: MapDispatchToPropsParam<IDispatchProps, IOwnProps>,
        mergeProps?: MergeProps<IStateProps, IDispatchProps, IOwnProps, IMergedProps>,
        options?: Options<IStateProps, IOwnProps, IMergedProps>,
    ): InferableComponentEnhancerWithProps<IMergedProps, IOwnProps>

}

export const connect = originalConnect as IConnect

Тогда мой подключенный компонент выглядит так:

import {connect} from 'path-to-my-connect/connect'

interface IOwnProps {
    ... props exposed for the real parent component
}

interface IStateProps {
    ... props from mapStateToProps
}

interface IDispatchProps {
    ... props from mapDispatchToProps
}

interface IProps extends IStateProps, IDispatchProps, IOwnProps {}

@connect<IStateProps, IDispatchProps, IOwnProps>(
    (state: IAppState) => {
            return {
                foo: getFoo(state), // getFoo is a selector using 'reselect'
            }
        },
    {setFoo, increment, decrement, ... your action creators},
)
class MyComponent extends React.PureComponent<IProps> {
        // your component implementation
}

export default (MyComponent as any) as React.ComponentType<IOwnProps>

Прямое приведение MyComponent as React.ComponentType<IOwnProps> не удастся, поэтому я сначала набрал его как any . Я знаю, что это взлом, но он хорошо работает как для родителя, так и для самого компонента.

Это наиболее жизнеспособное, но все же ограничивающее решение, которое я смог придумать.

"react-redux": "^5.0.6",
"typescript": "^2.8.1",
"@types/react-redux": "^6.0.0",

любые новости?

@ctretyak нет, вам в конечном итоге придется создать файл декларации самостоятельно и изменить декоратор. Похоже, что React Eco и TS не лучшие друзья, поэтому я отказался от него.

const DecoratedComponent = require('../DecoratedComponent').default;

этот вид импорта позволяет IDE отображать реквизиты и скрывать ошибки ts. немного некрасиво, но декораторы выглядят лучше, чем простое подключение. особенно если вы настроили автоматическое добавление какого-либо селектора, такого как переводы или навигация

Это не проблема реакции-редукции. У меня такое же поведение с @withRouter (от React-Router) и @graphql

Typescript, кажется, не понимает, что декоратор может вводить реквизиты ...

Typescript, кажется, не понимает, что декоратор может вводить реквизиты ...

Ух, это все еще проблема? Просто столкнулся с этим.

Мне грустно, что что-то разрушает наш мозг, но они были созданы, чтобы нам было удобнее :(

Мне грустно, что что-то разрушает наш мозг, но они были созданы, чтобы нам было удобнее :(

я тоже :(

Это, вероятно, не охватывает все варианты использования, но у меня хорошо работает.
Это также дает мне возможность добавить мой тип магазина (MyAppStore) в одном месте.

export function connectTs<TDispatchProps={}, TOwnProps=any>(
  mapStateToProps?: (state: MyAppStore, ownProps?: TOwnProps) => any,
  mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>
): any {
  return connect(mapStateToProps, mapDispatchToProps)
}

у вас есть обновления?

Я бы тоже хотел использовать connect как декоратор.

Какие-то официальные решения поставляются ???
Я думаю, что многие разработчики предпочитают использовать @connect вместо подключения (mapStateToProps, mapDispatchToProps) (компоненты)

Была ли эта страница полезной?
0 / 5 - 0 рейтинги