reactjs - ¿Puede mapStateToProps restringir los tipos de props?
Estoy intentando agregar anotaciones de tipo a un componente antiguo, y no estoy seguro de si los tipos de connect admiten lo que estoy intentando hacer. El componente es un componente conectado que recibe una propiedad con un tipo amplio, luego usa esa propiedad para buscar un valor en el estado y devuelve una propiedad más específica.
En este momento, puedo definir las props con el tipo amplio y el código funcionará. Sin embargo, cualquier código que escriba dentro del componente tiene que ajustarse a este tipo amplio, aunque sepa que ha sido reducido. Por lo tanto, para satisfacer a TypeScript, o tengo que añadir afirmaciones de tipo, o comprobaciones de tipo innecesarias.
Aquí hay un ejemplo simplificado del código para mostrar el comportamiento:
interface ExampleProps {
thing: string | number;
}
const Example extends React.Component<ExampleProps> {
render() {
const problem = this.props.thing * 2; // type error
// Workaround, but losing some typesafety
// const notAProblem = (this.props.thing as number) * 2;
// Workaround, but doing unnecessary checks
// if (typeof this.props.thing === 'number') {
// const notAProblem = this.props.thing * 2;
// }
return null;
}
}
const mapStateToProps = (state: RootState, otherProps: ExampleProps) => {
if (typeof otherProps.thing === 'string') {
return {
thing: state.lookup[otherProps.thing]; //resolves to a number
}
}
return {
thing, // also a number
}
}
export default connect(mapStateToProps)(Example)
En el código anterior, typescript señala correctamente que const problem: number=this.props.thing * 2; es un problema. Con los tipos que he definido, this.props.thing podría ser una cadena, por lo que correctamente da el error El lado izquierdo de una operación aritmética debe ser de tipo 'any', 'number', 'bigint' o un tipo enum.
¿Es posible usar connect de tal manera que lleguen diferentes props de los que salen? En otras palabras, necesito que alguien pueda renderizar <Example thing={2} /> o <Example thing={"2"} />, y aun así que el código dentro del Example sepa que, gracias a mapStateToProps, thing solo puede ser un number
Respuesta #1
Enfoque 1: Tipo genérico por campo
Un enfoque sería hacer que "thing" sea un tipo genérico en ExampleProps, es decir:
interface ExampleProps<T extends (string | number)> {
thing: T;
}
Entonces podrías simplemente especificar diferentes tipos concretos en las firmas de tipo de la clase y la función, respectivamente:
class Example extends Component<ExampleProps<number>> {
render() {
const problem: number = this.props.thing * 2;
return null;
}
}
const mapRootStateToExampleProps = (state: RootState, {thing}: ExampleProps<string | number>) => {
if (typeof thing === 'string') {
return {
thing: state.lookup[thing] //resolves to a number
}
}
return {
thing, // also a number
}
}
Sin embargo, esto puede no ser una solución óptima, ya que tus firmas de tipo podrían volverse bastante verbosas rápidamente. Realmente solo tiene sentido en uno o dos campos de la interfaz ExampleProps.
Enfoque 2: Interfaz base para parámetros "out", los parámetros "in" extienden la base
Una alternativa podría ser declarar una interfaz base que represente las props que entran, y luego una interfaz que la extienda representando las props que salen; así solo tienes que especificar los tipos que se modifican al "salir", por lo que no estás repitiendo mucho código:
interface ExamplePropsIn {
thing: string | number;
other: string;
}
interface ExamplePropsOut extends ExamplePropsIn {
thing; number;
}
class Example extends Component<ExamplePropsOut> {
render() {
const problem: number = this.props.thing * 2;
return null;
}
}
const mapRootStateToExampleProps = (state: RootState, {thing}: ExamplePropsIn) => {
if (typeof thing === 'string') {
return {
thing: state.lookup[thing] //resolves to a number
}
}
return {
thing, // also a number
}
}
