Definitelytyped: `connect` de react-redux no se puede usar como decorador: el tipo" no se puede asignar al tipo 'void' "

Creado en 4 jul. 2016  ·  32Comentarios  ·  Fuente: DefinitelyTyped/DefinitelyTyped

Intentar usar connect react-redux como decorador de clases puede causar mensajes de error de la forma Type 'foo' is not assignable to type 'void' . El problema con estas tipificaciones parece ser que TypeScript no permite que ClassDecorator s cambie la firma de lo que decoran , pero esta es la implementación actual de las tipificaciones react-redux y se hace a propósito, ya que react-redux devuelve una instancia diferente con una firma diferente para los accesorios .

El uso de connect como función funciona como se esperaba; es solo el uso como decorador lo que causa problemas de tipografía.

Las sugerencias son bienvenidas, pero cualquier solución propuesta debe ser una mejora estricta de lo que las tipificaciones pueden expresar actualmente, es decir, sacrificar la corrección actual de las tipificaciones de uso como función no es aceptable (en parte porque eso sería un problema). regresión severa y en parte porque los decoradores se consideran "experimentales" y, por lo tanto, afirmo, secundarios al uso de funciones estándar).

No sé si es posible una solución completa al problema de mecanografía del decorador, mientras que https://github.com/Microsoft/TypeScript/issues/4881 es excepcional. Dicho esto, hay mejoras incrementales en este caso, como al (primer paso) generar cualquier tipo de React.ComponentClass para que el código al menos se compile, luego (segundo paso) generar un componente que acepte la intersección de todos los props (propios y conectados), aunque eso sería demasiado indulgente, por lo que el tipo de código verifica al menos algunos de los props.

La única solución que tengo ahora es no usar connect como decorador. Algunos pueden encontrarlo feo desde el punto de vista estilístico, pero revisa el tipo correctamente, no es más extenso y se puede usar en más lugares que un decorador.

En lugar de:

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

use (algo parecido a):

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

Tenga en cuenta que https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8787 entra en juego aquí y, a veces, se combina con este problema. Es posible que también desee una sugerencia de tipo o una conversión para asegurarse de que el componente de salida tenga el tipo adecuado.

Editar: https://github.com/Microsoft/TypeScript/pull/12114 puede ayudar con este caso: el decorador podría potencialmente escribirse para crear una versión totalmente opcional de los accesorios para el componente generado, que no es totalmente ideal pero continúa permitiendo que su implementación sea más asertiva sobre la nulidad de la propiedad.

Comentario más útil

Para usar @connect directamente (sin introducir decoradores personalizados) utilizo la siguiente solución:

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

Pero estoy totalmente de acuerdo con @seansfkelley en que la escritura no es realmente lo que queremos tener ...

Todos 32 comentarios

Tuve problemas similares el otro día y lo abandoné por la función de conexión. Los tipos miraron hacia fuera y usaron viejos tipos de reacción. ¿Puedo intentar solucionar este problema y enviar un PR?

¡Por supuesto! Mucho ha cambiado desde que se escribieron originalmente estos tipos, aunque todavía soy escéptico de que sin el soporte del decorador mutante es posible algo útil. Pero he solucionado otras deficiencias en el sistema de tipos antes, así que pruébalo.

¿Quizás los tipos mapeados podrían llevarlo parte del camino allí, por lo que al menos tiene _alguna_ información de tipo útil en la salida?

YOLO

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

No creo que tenga mucho sentido escribir esto en yolo, porque las soluciones alternativas enumeradas aquí y en el n. ° 8787, específicamente la de usar las sugerencias para la llamada a la función, no son tan malas (especialmente con las plantillas IDE para clases de componentes) y tener un componente de cualquier tipo es triste. :( Cualquier tipo de conexión solo para usarlo como decorador en lugar de una llamada de función es realmente poner el carro antes que el caballo.

Para usar @connect directamente (sin introducir decoradores personalizados) utilizo la siguiente solución:

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

Pero estoy totalmente de acuerdo con @seansfkelley en que la escritura no es realmente lo que queremos tener ...

A pesar de que la sintaxis del decorador es genial, sigo pensando que usar HOC como conectar de manera estándar es una mejor práctica.

1) Modifica el tipo de una clase que va en contra de la especificación del decorador.
2) Connect no es un decorador en absoluto, es un HOC.
3) Rompe la portabilidad del código al tener una sintaxis diferente para componentes sin estado y con estado.

Así que me gustaría decir, por favor, no proporcione una definición que permita a los desarrolladores usarla de manera incorrecta :)

@npirotte De acuerdo. Tiene toda la razón, el uso de HOC como decoradores viola las especificaciones de los decoradores. Ya no es la clase base sino una completamente diferente.

Tengo exactamente los mismos problemas con los decoradores personalizados y estoy viendo el estado de su soporte en TS (todavía están detrás de la bandera y AFAICS, no hay planes para habilitarlos de forma predeterminada) .Estoy a punto de abandonar el soporte para ellos. Además, los decoradores todavía están en el borrador de w3c.

La mejor manera de ajustar un componente a uno nuevo es usar una función de orden superior para él en lugar de intentar _decorar_ la clase existente.

Tengo el mismo error si lo importo. Sin embargo, lo siguiente funciona para mí:

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

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

@TriStarGod eso depende de cómo hayas escrito require . Parece probable que termine con connect escrito como any en ese caso.

Otra solución / solución alternativa.

Como ya tengo mi propio estado de aplicación, necesito una función muy corta en forma de:

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> {
...
}

Y aquí está ese método de conexión envuelto:

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
}

Gracias por tu trabajo. Suscribiéndose y esperando avances en este tema.

@offbeatful , proporcione una declaración de tipo de otra variación de mapDispatchToProps

Si se pasa un objeto, se asume que cada función dentro de él es un creador de acciones de Redux. Un objeto con los mismos nombres de función, pero con cada creador de acciones envuelto en una llamada de despacho para que puedan ser invocados directamente, se fusionará con los accesorios del componente.

otra solución alternativa basada en el comentario de @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;

Actualizado según el fragmento de @pravdomil y los tipos más recientes (5.0.13)

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;

Hola @offbeatful (cc: @pravdomil, ¿supongo?), Recientemente hice una pregunta de Stack Overflow relacionada con este problema en la que se concluyó que actualmente no es compatible. Como parte de esa pregunta, preparé un repositorio para mostrar lo que probé. Se concluyó que esto no es compatible actualmente, así que estaba emocionado de ver su fragmento de código hoy y seguí adelante para intentar actualizar mi repositorio para usarlo.

Aquí puede ver la confirmación con su fragmento de código integrado

Esto ya no grita con los errores que recibía antes, pero descubrí que en mi caso de uso donde mi componente está conectado, pero también tiene sus propios accesorios, esta nueva firma ahora hará que incluso los accesorios de estado sean necesarios como props propios (TSX). cd my-app && yarn start en mi repositorio mostrará lo que quiero decir.

¿Crees que esto es algo que también se puede abordar o no es posible?

posiblemente puede estar usando esta versión connect

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

Si es así, solo necesita arreglar el parámetro options para que sea Options<any, any, any> esta manera no tendrá el problema TStateProps & TDispatchProps porque será TStateProps & any

en una palabra:

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

genial como tu

@TomasHubelbauer Acabo de probar su método, si vuelve al método de conexión de redux predeterminado
Funcionará de manera bastante extraña.

Es mejor asegurarse de utilizar tipos compartidos entre los tres:

export type CounterStateProps = {
    count: number;
};

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

export type CounterOwnProps = {
    initialCount: number;
};

export type CounterProps = CounterStateProps & CounterDispatchProps & CounterOwnProps;

Luego implemente el componente con estado

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

Y finalmente haga que la clase del conector sea así usando la compilación de redux en conectar NO el código de conexión personalizado.

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 Sí, sé que esto funciona, en mi ejemplo lo mostré antes de probar algunas de las sugerencias para que funcione como decorador. Utilizo la variante sin decorador con éxito, pero realmente me gustaría hacer que el decorador también funcione.

Además, creo que mezcló sus tipos, usa CounterStateProps (que es el tipo de retorno de mapDispatchToProps como un componente de CounterProps (lo cual está bien, tiene el estado Redux puntales, puntales de despacho y Redux JSX propios accesorios), sino también como un tipo para el estado de los componentes. en lugar state debe tener su propio tipo que no está implicado en la firma de tipo de componente fuera de ninguna manera.

Es posible que no entendiera su solución por completo, así que si puede hacer que esto funcione en mi repositorio (donde utilizo ambos props propios en TSX y los props de estado de Redux) sin obtener un error que diga que necesita especificar los props de estado de Redux en TSX ¡También sería genial!

+1 en este tema

Actualicé el fragmento de

connect.ts ( IAppState es la interfaz del estado redux)

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

Entonces mi componente conectado se ve así:

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>

La transmisión directa MyComponent as React.ComponentType<IOwnProps> fallará, así que primero lo escribí como any . Sé que es un truco, pero funciona bien para el padre y también para el componente en sí.

Esta es la solución más viable, pero aún restrictiva, que he podido encontrar.

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

¿hay noticias?

@ctretyak no, eventualmente tendrás que crear un archivo de declaración tú mismo y modificar el decorador. Parece que React Eco y TS no son los mejores amigos, por eso lo dejé.

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

este tipo de importación permite al IDE mostrar accesorios y ocultar el error ts. un poco feo, pero los decoradores se ven mejor que simplemente conectar. especialmente si configura para agregar algún selector automáticamente como traduce o navegación

No es un problema de reacción-reducción. Tengo el mismo comportamiento con @withRouter (de React-Router) y @graphql

Mecanografiado parece no entender que un decorador podría inyectar accesorios ...

Mecanografiado parece no entender que un decorador podría inyectar accesorios ...

Uf, ¿esto sigue siendo un problema? Me encontré con esto.

Me entristece que algo esté destruyendo nuestro cerebro, pero fueron diseñados para hacernos sentir más cómodos :(

Me entristece que algo esté destruyendo nuestro cerebro, pero fueron diseñados para hacernos sentir más cómodos :(

Yo también :(

Esto probablemente no cubre todos los casos de uso, pero me ha funcionado bien.
También me da la oportunidad de agregar mi tipo de tienda (MyAppStore) en un solo lugar.

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

¿tienes alguna actualización?

También me gustaría usar connect como decorador.

¿Alguna solución oficial suministrada ???
Creo que hay muchos desarrolladores que prefieren usar @connect en lugar de connect (mapStateToProps, mapDispatchToProps) (componentes)

¿Fue útil esta página
0 / 5 - 0 calificaciones