
Actualmente, el desarrollo de cualquier aplicación front-end moderna es más complicado que el nivel hello world
, en el que trabaja el equipo (cuya composición cambia periódicamente), exige mucho la calidad de la base del código. Para mantener el nivel de calidad del código en el nivel adecuado, en el equipo front - end #gostgroup nos mantenemos actualizados y no tenemos miedo de utilizar tecnologías modernas que muestren su beneficio práctico en proyectos de empresas de diversas escalas .
La tipificación estática y sus beneficios en el ejemplo de TypeScript se han discutido mucho en varios artículos y, por lo tanto, hoy nos centraremos en tareas más aplicadas a las que se enfrentan los desarrolladores front-end como un ejemplo de la pila favorita de nuestro equipo (React + Redux).
"No entiendo cómo vives en general sin una mecanografía estricta. ¿Qué estás haciendo? ¿Débito por días y días?" - El autor no me es conocido.
"no, escribimos tipos todo el día" - mi colega.
Al escribir código en TypeScript (en adelante, en el texto, la pila de temas estará implícita), muchos se quejan de que tienen que pasar mucho tiempo escribiendo tipos manualmente. Un buen ejemplo que ilustra el problema es la función de conector de connect
de la react-redux
. Echemos un vistazo al código a continuación:
type Props = { a: number, b: string; action1: (a: number) => void; action2: (b: string) => void; } class Component extends React.PureComponent<Props> { } connect( (state: RootStore) => ({ a: state.a, b: state.b, }), { action1, action2, }, )(Component);
¿Cuál es el problema aquí? El problema es que para cada nueva propiedad inyectada a través del conector, debemos describir el tipo de esta propiedad en el tipo general de propiedades de componentes (React). No es una ocupación muy interesante, dices, todavía quieres poder recopilar el tipo de propiedades del conector en un tipo, que luego se "conecta" una vez al tipo general de propiedades del componente. Tengo buenas noticias para ti. ¡TypeScript te permite hacer esto hoy! Estas listo Vamos!
El poder de TypeScript
TypeScript no se detiene y evoluciona constantemente (por lo que me encanta). A partir de la versión 2.8, apareció una función muy interesante (tipos condicionales), que permite la asignación de tipos basados en expresiones condicionales. No entraré en detalles aquí, simplemente deje un enlace a la documentación e inserte un fragmento de código como ilustración:
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T0 = TypeName<string>;
Cómo ayuda esta función en nuestro caso. react-redux
observar la descripción de los tipos de biblioteca react-redux
, puede encontrar el tipo InferableComponentEnhancerWithProps
, que es responsable de garantizar que los tipos de propiedades inyectadas no caigan en el tipo externo de propiedades del componente que debemos establecer explícitamente al instanciar el componente. El tipo InferableComponentEnhancerWithProps
tiene dos parámetros generales: TInjectedProps
y TNeedsProps
. Estamos interesados en lo primero. ¡Intentemos "extraer" este tipo de este conector!
type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ;
Y extrayendo directamente el tipo en un ejemplo real del repositorio (que puede clonar y ejecutar un programa de prueba allí):
import React from 'react'; import { connect } from 'react-redux'; import { RootStore, init, TypeOfConnect, thunkAction, unboxThunk } from 'src/redux'; const storeEnhancer = connect( (state: RootStore) => ({ ...state, }), { init, thunkAction: unboxThunk(thunkAction), } ); type AppProps = {} & TypeOfConnect<typeof storeEnhancer> ; class App extends React.PureComponent<AppProps> { componentDidMount() { this.props.init(); this.props.thunkAction(3000); } render() { return ( <> <div>{this.props.a}</div> <div>{this.props.b}</div> <div>{String(this.props.c)}</div> </> ); } } export default storeEnhancer(App);
En el ejemplo anterior, dividimos la conexión al repositorio (Redux) en dos etapas. En la primera etapa, obtenemos un componente de orden superior storeEnhancer
(también InferableComponentEnhancerWithProps
como InferableComponentEnhancerWithProps
) para extraer tipos de propiedades InferableComponentEnhancerWithProps
de él usando nuestro TypeOfConnect
auxiliar TypeOfConnect
y combinar aún más (a través de la intersección de &
tipos) los tipos de propiedades obtenidos con los tipos de propiedades nativas del componente. En la segunda etapa, simplemente decoramos nuestro componente original. Ahora, lo que agregue al conector, se incluirá automáticamente en los tipos de propiedad del componente. ¡Genial, lo que queríamos lograr!
Un lector atento notó que los generadores de acción (para abreviar, simplificamos el término para acción) con efectos secundarios (creadores de acción thunk) se someten a un procesamiento adicional utilizando la función unboxThunk
. ¿Qué causó tal medida adicional? Vamos a hacerlo bien. Primero, veamos la firma de tal acción usando el ejemplo de un programa del repositorio:
const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); };
Como se puede ver en la firma, nuestra acción no devuelve de inmediato la función de destino, sino primero una función intermedia, que se redux-middleware
para permitir efectos secundarios en nuestra función principal. Pero cuando se usa esta función en la forma conectada en las propiedades del componente, la firma de esta función se reduce, excluyendo la función intermedia. ¿Cómo describirlo en tipos? Necesita una función de convertidor especial. Una vez más, TypeScript muestra su poder. Primero, describimos el tipo que elimina la función intermedia de la firma:
CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ;
Aquí, además de los tipos condicionales, se utiliza una innovación muy reciente de TypeScript 3.0, que le permite mostrar el tipo de un número arbitrario (parámetros de reposo) de argumentos de función. Consulte la documentación para más detalles. Ahora queda por eliminar el exceso de nuestra acción de una manera bastante rígida:
const unboxThunk = <Args extends any[], R, S, E, A extends Action<any>>( thunkFn: (...args: Args) => ThunkAction<R, S, E, A>, ) => ( thunkFn as any as CutMiddleFunction<typeof thunkFn> );
Al pasar la acción a través de dicho convertidor, obtenemos la firma que necesitamos en la salida. Ahora la acción está lista para usar en el conector.
Entonces, a través de manipulaciones simples, reducimos nuestro trabajo manual al escribir código escrito en nuestra pila. Si vamos un poco más allá, también podemos simplificar la tipificación de acciones y reductores, como lo hicimos en redux-modus .
PD Al utilizar el enlace dinámico de acciones en el conector a través de la función y redux.bindActionCreators
tendremos que encargarnos de escribir más adecuadamente esta utilidad (posiblemente escribiendo nuestro propio contenedor).
Actualización 0
Si alguien pensó que esta solución era conveniente, entonces le puede gustar para que la utilidad type se agregue al @types/react-redux
.
Actualización 1
Algunos tipos más con los que no necesita especificar explícitamente el tipo de accesorios inyectados del corvejón. Solo toma el hoki y saca los tipos de ellos:
export type BasicHoc<T> = (Component: React.ComponentType<T>) => React.ComponentType<any>; export type ConfiguredHoc<T> = (...args: any[]) => (Component: React.ComponentType<T>) => React.ComponentType<any>; export type BasicHocProps<T> = T extends BasicHoc<infer Props> ? Props : never; export type ConfiguredHocProps<T> = T extends ConfiguredHoc<infer Props> ? Props : never; export type HocProps<T> = T extends BasicHoc<any> ? BasicHocProps<T> : T extends ConfiguredHoc<any> ? ConfiguredHocProps<T> : never ; const basicHoc = (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; const configuredHoc = (opts: any) => (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; type props1 = HocProps<typeof basicHoc>;
Actualización2
El tipo del sujeto ahora está en @types/react-redux
( ConnectedProps ).