Definitelytyped: Implementación de defaultProps con comprobaciones nulas estrictas de ts 2.0

Creado en 30 sept. 2016  ·  50Comentarios  ·  Fuente: DefinitelyTyped/DefinitelyTyped

Las propiedades predeterminadas no parecen funcionar bien actualmente con las comprobaciones estrictas de nulos habilitadas. Por ejemplo:

interface TestProps { x?: number}

class Test extends React.Component<TestProps, null> {

    static defaultProps =  {x: 5};

    render() {
        const x: number = this.props.x;
        return <p>{x}</p>;
    }
}

Errores con error TS2322: Type 'number | undefined' is not assignable to type 'number' aunque se garantiza que funcionará en tiempo de ejecución.

En este momento, DefaultProps y Props parecen tratarse siempre como el mismo tipo, pero en realidad casi nunca son del mismo tipo porque los campos opcionales en Props deben sobrescribirse con los valores requeridos en DefaultProps.

¿Y si hubiera algo como...

class ComponentWithDefaultProps<P, D, S> {
    props: P & D & {children?: React.Children};
}

¿Eso es idéntico a la escritura React.Component existente excepto por el tipo de accesorios?

Comentario más útil

Si alguien tiene una buena solución para tipos y accesorios predeterminados, soy todo oídos. Actualmente hacemos esto:

interface Props {
  firstName: string;
  lastName?: string;
}

interface DefaultProps {
  lastName: string;
}

type PropsWithDefaults = Props & DefaultProps;

export class User extends React.Component<Props> {
  public static defaultProps: DefaultProps = {
    lastName: 'None',
  }

  public render () {
    const { firstName, lastName } = this.props as PropsWithDefaults;

    return (
      <div>{firstName} {lastName}</div>
    )
  }
}

Todos 50 comentarios

Dado que los accesorios predeterminados se establecen en tiempo de ejecución, no estoy seguro de si hay una manera de manejar esto bien, que no sea una afirmación de tipo. (Por supuesto, siempre puede deshabilitar las comprobaciones nulas estrictas).

Así es como puede evitarlo en su ejemplo:

interface TestProps { x?: number}

class Test extends React.Component<TestProps, null> {

    static defaultProps =  {x: 5};

    render() {
        const x: number = (this.props.x as number);
        return <p>{x}</p>;
    }
}

Consulte https://www.typescriptlang.org/docs/handbook/basic-types.html#type -assertions

Si hay una forma más elegante de manejar esto, me encantaría escucharla.

Descargo de responsabilidad: he estado usando TypeScript durante unos tres días, estoy cansado y probablemente no tenga idea de lo que estoy hablando.

+1000 en la actualización del archivo de definición de tipos para incluir tres definiciones de tipos genéricos.

Esto solía estar bien sin ---strictNullChecks , pero ahora definitivamente será un problema para muchas clases de componentes.

Flow también implementa una implementación de clase similar debido a la naturaleza de la verificación estricta de tipos nulos.
https://github.com/facebook/flow/blob/master/lib/react.js#L16
https://github.com/facebook/flow/blob/master/lib/react.js#L104 -L105

Parece que no tenemos muchas opciones aquí, excepto esperar a que se resuelva https://github.com/Microsoft/TypeScript/issues/2175 para agregar un tercer genérico.
No creo que los revisores puedan aprobar un cambio tan importante (me refiero a class Component<P, S, D> ).
@johnnyreilly @bbenezech @pzavolinsky , ¿tienen alguna opinión sobre eso?

@r00ger estuvo de acuerdo. Cambiar la definición es demasiado perturbador.

¿Alguien ha considerado usar Partial ?

Como en:

    interface ComponentClass<P> {
-        defaultProps?: P;
+        defaultProps?: Partial<P>;
    }

No te preocupes Partial arriba.

Partial solo resuelve cómo declarar el problema de propTypes parcial. Dentro de render , lastName sigue siendo del tipo string | undefined . Para evitarlo, debe lanzar la cadena usando as o ! como se muestra a continuación. Funciona, pero no es ideal.

interface IUser {
    firstName: string
    lastName?: string
}
export class User extends React.Component<IUser, {}> {
    public static defaultProps: Partial<IUser> = {
        lastName: 'None',
    }

    public render () {
        const { firstName, lastName } = this.props
        // error
        lastName.toUpperCase()

        return (
            <div>{firstName} {lastName}</div>
        )
    }
}

Acabo de empezar a usar TS. ¿Me estoy perdiendo algo?

Si alguien tiene una buena solución para tipos y accesorios predeterminados, soy todo oídos. Actualmente hacemos esto:

interface Props {
  firstName: string;
  lastName?: string;
}

interface DefaultProps {
  lastName: string;
}

type PropsWithDefaults = Props & DefaultProps;

export class User extends React.Component<Props> {
  public static defaultProps: DefaultProps = {
    lastName: 'None',
  }

  public render () {
    const { firstName, lastName } = this.props as PropsWithDefaults;

    return (
      <div>{firstName} {lastName}</div>
    )
  }
}

+1
Actualmente estoy luchando contra este problema.

+1

+1

Además de agregar el tercer parámetro de tipo, necesitará la capacidad de diferenciar accesorios contra los accesorios predeterminados. ¡Felizmente a partir de TS 2.4 esto ahora es posible! Consulte https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340

En mi humilde opinión, agregar un tercer parámetro es un gran no, no, también el equipo de Flow lo sabía y recientemente lo cambiaron por un bien mayor. Debería ser responsabilidad del verificador de tipos saber cómo manejar este tipo de cosas.

No me malinterpreten, me encanta Typescript, pero desde Flow 0.53 debo decir que es superior para el desarrollo de React https://medium.com/flow-type/even-better-support-for-react-in-flow- 25b0a3485627

@Hotell Flow tiene tres parámetros de tipo para React.Component : según el artículo de Medium que vinculó a Flow, puede inferir parámetros de tipo de clase a partir de las anotaciones de la subclase: una característica ordenada de nivel de idioma que TS no admite, pero no es un tipo -consideración de declaración AFAIK.

@aldendaniels

El flujo tiene tres parámetros de tipo para React.Component

no, solía ser así antes de la 0.53, ya no :)

@Hotell ¡ Ah, por supuesto! Gracias por corregirme.

AFAIK, sin embargo, no hay forma en TS de inferir el tipo de accesorios predeterminados. Con el enfoque de tres tipos de parámetros, es probable que podamos escribir correctamente sin bloquear los cambios anteriores del equipo de TypeScript.

¿Conoce alguna forma de usar el tipo inferido de una propiedad estática sin pasar typeof MyComponent.defaultProps como parámetro de tipo?

¿Alguna noticia sobre este tema? ¿Alguien hace una PR para agregar un tercer parámetro de tipo y usar https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340?

Problema de votación positiva: el mismo problema

+1

También me encontré con esto, y elegí (hasta que esto se solucione correctamente) abstenerme de usar static defaultProps y en su lugar usar un HOC auxiliar:

Componentes del archivo/ayudantes/withDefaults.tsx :

import * as React from 'react'

export interface ComponentDefaulter<DP> {
  <P extends {[key in keyof DP]?: any}>(Component: React.ComponentType<P>): React.ComponentType<
    Omit<P, keyof DP> &         // Mandate all properties in P and not in DP
    Partial<Pick<P, keyof DP>>  // Accept all properties from P that are in DP, but use type from P
  >
}

export default function withDefaults<DP>(defaultProps: DP): ComponentDefaulter<DP> {
  return Component => props => <Component {...defaultProps} {...props}/>
}

Ahora puedo usar:

Componentes del archivo/Button.tsx :

import * as React from 'react'
import withDefaults from './helpers/withDefaults'

export interface ButtonProps {
  label: string
  onPress: () => any
}

export const defaultProps = {
  onPress: () => undefined
}

class Button extends React.Component<ButtonProps> {
  // ...
}

export default withDefaults(defaultProps)(Button)

Tres desventajas potenciales (que se me ocurren):

  1. Requiere un HOC, pero dado que este es un paradigma bastante común en el mundo de React, parece estar bien.
  2. Debe declarar los accesorios como un parámetro de tipo genérico y no puede confiar en la inferencia de la propiedad props .
  3. No hay verificación implícita de los tipos de defaultProps , pero esto se puede remediar especificando export const defaultProps: Partial<ButtonProps> = {...} .

Según @vsaarinen , escribo una clase base con props: Props & DefaultProps , por lo que toda la clase que extiende la clase base puede usar directamente this.props sin usar this.props as PropsWithDefaults .

Me gusta esto:

import * as React from 'react'

export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
  props: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}

export interface Props {
  firstName: string
  lastName?: string
}

export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, any, DefaultProps> {
  render() {
    const { firstName, lastName } = this.props

    // no error
    return (
      <div>{firstName} {lastName.toUpperCase()}</div>
    )
  }
}

En realidad , @ qiu8310 que no funcionó completamente, todavía tenía problemas con los sitios de llamadas que gritaban que esos accesorios predeterminados no eran opcionales. Conseguí que funcionara con un pequeño ajuste.

import * as React from 'react'

export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
  // Cast the props as something where readonly fields are non optional
  props = this.props as Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}

export interface Props {
  firstName: string
  lastName?: string
}

export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, any, DefaultProps> {
  render() {
    const { firstName, lastName } = this.props

    // no error
    return (
      <div>{firstName} {lastName.toUpperCase()}</div>
    )
  }
}

Jugué con el tercer genérico y terminé teniendo algo similar a la propuesta de @qiu8310 :

// ComponentWithDefaultProps.ts
import * as React from "react";

export declare class ComponentWithDefaultProps<P, S, DP extends Partial<P>> extends React.Component<P & DP, S> {}
type redirected<P, S, DP> = ComponentWithDefaultProps<P, S, DP>;
const redirected: typeof ComponentWithDefaultProps = React.Component as any;

export const Component = redirected;

// User.ts
import { Component } from "ComponentWithDefaultProps";
export interface Props {
  firstName: string
  lastName?: string
}
export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, {}, DefaultProps> {
  public render() {
    const { firstName, lastName } = this.props;
    return <div>{firstName} {lastName.toUpperCase()}</div>;
  }
}

Sin embargo, ambos enfoques (el mío y el anterior) causan un problema mayor. Hay tipos de componentes creados en mi ejemplo:

User: React.ComponentClass<P & DP>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P & DP>

Aparentemente, la interfaz User es incorrecta. React.ComponentClass<P & DP> significa que también se requiere lastName , por lo que

<User firstName="" />;
//    ~~~~~~~~~~~~  Property 'lastName' is missing...

En el ejemplo de @qiu8310 , los tipos son diferentes:

User: React.ComponentClass<P>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>

Pero la misma pieza de JSX provoca el mismo error, porque las comprobaciones JSX tsc se basan en el tipo props .

<User firstName="John" />;
//    ~~~~~~~~~~~~~~~~  Property 'lastName' is missing...

Lo divertido es que <User firstName="John" /> se está convirtiendo en React.createElement(User, {firstName: "John"}) que sería un TypeScript válido. En ese caso, las comprobaciones de tipo se basan en el primer parámetro de tipo ComponentClass , por lo que

<User firstName="Jonh" />; // doesn't work, but
React.createElement(User, { firstName: "John" }); // works

Como puede ver, incluso teniendo un tercer genérico, todavía tenemos que agregar otro truco para poder exportar un componente con la interfaz correcta:

export const User = class extends Component<Props, {}, DefaultProps> {
    // ...
} as React.ComponentClass<Props>;

<User firstName="Jonh" />; // works

Así que tener un tercer genérico no tiene mucho sentido.

Parece que no hay una buena solución que pueda fusionarse con la definición de React , por ahora me quedo con ComponentWithDefaultProps y afirmando el tipo de componente exportado.

export interface DefaultProps {
    lastName: string;
}
export interface Props extends Partial<DefaultProps> {
    firstName: string;
}

export type PropsWithDefault = Props & DefaultProps;

export const User: as React.ComponentClass<Props> =
class extends React.Component<PropsWithDefault> {
    render() {
        // no error
        return <div>
            {this.props.firstName}
            {this.props.lastName.toUpperCase()}
        </div>;
    }
};
// Note, we've assigned `React.Component<PropsWithDefault>` to `React.ComponentClass<Props>`

Aparte de eso, puede afirmar todos los usos del tipo this.props en los métodos del componente (por ejemplo const { lastName } = this.props as Props & DefaultProps , o usar el signo de exclamación en todas partes this.props.lastName!.toLowerCase() ).

Encontré un ejemplo sobre esta discusión: https://github.com/gcanti/typelevel-ts#objectdiff

@rifler llamado enfoque HOC (prefiero decorador) ha estado aquí por un tiempo , tratamos de encontrar una solución que no agregue sobrecarga de tiempo de ejecución

Oh, genial
Espero que encuentres la solución.

¿cualquier progreso?

La siguiente es una variación de la técnica mencionada por @r00ger :

interface IUser {
    name: string;
}
const User = class extends React.Component<IUser> {
    public static defaultProps: IUser = {name: "Foo"}
    public render() {
        return <div>{this.props.name}</div>;
    }
} as React.ComponentClass<Partial<IUser>>;
React.createElement(User, {}); // no error, will output "<div>Foo</div>"

Usar el fragmento anterior funcionará, pero perderá la capacidad de usar propiedades estáticas en Usuario, ya que se convierte en una clase anónima. Una solución hacky sería sombrear el nombre de la clase, así:

// tslint:disable-next-line:no-shadowed-variable
const User = class User extends React.Component<IUser>

Ahora puede usar campos estáticos privados dentro de la clase. Las estáticas públicas siguen siendo inutilizables. Además, observe la necesidad de silenciar a tslint.

Pensé que valía la pena mencionar que a partir de TS 2.8, el tipo Exclude es oficialmente compatible:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

Consulte https://github.com/Microsoft/TypeScript/pull/21847.

Entonces, todo lo que necesitamos es que React.createElement() requiera lo siguiente en lugar de Props :

Omit<Props, keyof DefaultProps>

El único problema es que en las declaraciones de React no hay un tipo DefaultProps ; para esto necesitamos un tercer parámetro de tipo O la capacidad de inferir el tipo de miembros estáticos como una característica del lenguaje.

Mientras tanto, hemos estado rodando con lo siguiente:

/**
 * The Create type allow components to implement a strongly thed create() function
 * that alows the caller to omit props with defaults even though the component expects
 * all props to be populated. The TypeScript React typings do not natively support these.
 */
export type Create<C extends BaseComponent<any, any>, D extends {} = {}> = (
  props?: typeHelpers.ObjectDiff<C['props'], D> & React.ClassAttributes<C>,
  ...children: React.ReactNode[]
) => React.ComponentElement<any, any>;

export interface DomPropsType {
  domProps?: domProps.DomProps;
}

export class BaseComponent<P, S = {}> extends React.Component<P & DomPropsType, S> {
  static create(props?: object, ...children: React.ReactNode[]) {
    return React.createElement(this, props, ...children);
  }

  constructor(props: P & DomPropsType, context?: any) {
  ...
}

Y todos nuestros componentes se ven como:

export class InsertObjectMenu extends BaseComponent<Props, State> {
  static create: Create<InsertObjectMenu, typeof InsertObjectMenu.defaultProps>;
  static defaultProps = {
    promptForImageUpload: true,
  };
  ...
}

Finalmente, tenemos una regla de pelusa que exige que el atributo create se declare en todos los componentes. No usamos JSX, entonces usamos:

InsertObjectMenu.create({...})

En lugar de React.createElement() .

Hemos estado utilizando este enfoque en una gran base de código durante casi un año con mucho éxito, pero nos encantaría adoptar JSX y esto es lo que nos está frenando.

Tanto tiempo invertido en este "simple tema". Dejaré esto aquí https://medium.com/@martin_hotell/ultimate-react-component-patterns-with-typescript-2-8-82990c516935 🖖

    interface Component<P = {}, S = {}, DP extends Partial<P>=P> extends ComponentLifecycle<P, S> { }
    class Component<P, S, DP extends Partial<P> = P> {
        constructor(props: P & DP, context?: any);

        // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
        // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
        // Also, the ` | S` allows intellisense to not be dumbisense
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

        forceUpdate(callBack?: () => void): void;
        render(): ReactNode;

        // React.Props<T> is now deprecated, which means that the `children`
        // property is not available on `P` by default, even though you can
        // always pass children as variadic arguments to `createElement`.
        // In the future, if we can define its call signature conditionally
        // on the existence of `children` in `P`, then we should  remove this.
        private __externalProps: Readonly<{ children?: ReactNode }> & Readonly<P>;
        props: Readonly<{ children?: ReactNode }> & Readonly<P> & DP;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }

    class PureComponent<P = {}, S = {}, DP extends Partial<P>=P> extends Component<P, S, P> { }


interface ElementAttributesProperty { __externalProps: {}; }

Mire cuidadosamente la última línea.

Con estos cambios podríamos tener

interface Props {
    a: string
    b?: string
    c?: string
}

class Comp extends React.Component<Props, {}, typeof Comp.defaultProps> {
    static defaultProps = {
        b: ''
    }

    render() {
        const {a, b, c} = this.props

        let res = a.concat(b)  // ok
        let res1 = a.concat(c) //fail

        return null
    }
}



const res1= <Comp a=''/> // ok
const res3 = <Comp /> // fail

Lo mejor que podemos obtener si usamos static defaultProps (el verificador ts debe cambiarse si queremos omitir typeof Comp.defaultProps ).
Otras opciones, ya se dijo: HOC, tipo casts.

Aquí está mi (muy feo) intento basado en la idea de https://medium.com/@martin_hotell/ultimate -react-component-patterns-with-typescript-2-8-82990c516935:

type ExtractProps<T> = T extends React.ComponentType<infer Q> ? Q : never;
type ExtractDefaultProps<T> = T extends { defaultProps?: infer Q } ? Q : never;
type RequiredProps<P, DP> = Pick<P, Exclude<keyof P, keyof DP>>;
type RequiredAndPartialDefaultProps<RP, DP> = Required<RP> & Partial<DP>;

type ComponentTypeWithDefaultProps<T> =
  React.ComponentType<
    RequiredAndPartialDefaultProps<
      RequiredProps<ExtractProps<T>, ExtractDefaultProps<T>>,
      ExtractDefaultProps<T>
    >
  >;

function withDefaultProps<T extends React.ComponentType<any>>(Comp: T) {
  return Comp as ComponentTypeWithDefaultProps<T>;
}
interface IProps {
  required: number;
  defaulted: number;
}

class Foo extends React.Component<IProps> {
  public static defaultProps = {
    defaulted: 0,
  };
}

// Whichever way you prefer... The former does not require a function call
const FooWithDefaultProps = Foo as ComponentTypeWithDefaultProps<typeof Foo>;
const FooWithDefaultProps = withDefaultProps(Foo);

const f1 = <FooWithDefaultProps />;  // error: missing 'required' prop
const f2 = <FooWithDefaultProps defaulted={0} />;  // error: missing 'required' prop
const f3 = <FooWithDefaultProps required={0} />;  // ok
const f4 = <FooWithDefaultProps required={0} defaulted={0} />;  // ok

@decademoon , parece que podríamos usar esta solución en @types/react , ¿verdad? Quiero decir, si reemplazamos el habitual React.ComponentType con su solución.
Si es así, ¿quizás puedas crear un PR?

@decademoon su definición no maneja el caso en el que los accesorios no predeterminados en realidad incluyen campos opcionales, es decir

interface IProps {
  required: number;
  notRequired?: () => void;
  defaulted: number;
}

class Foo extends React.Component<IProps> {
  public static defaultProps = {
    defaulted: 0,
  };
}

Lo hice funcionar en mi caso cambiando su tipo RequiredAndPartialDefaultProps para no envolver el "RP" con "Required"

type RequiredAndPartialDefaultProps<RP, DP> = RP & Partial<DP>;

Me sorprende que todavía no haya una solución adecuada o al menos un HOC que funcione en NPM; a menos que me haya perdido algo.

Hola a todos. Solo quería decir y si todavía estás leyendo este hilo: creo que @JoshuaToenyes hizo la explicación más significativa y útil. Esto definitivamente no es un problema, así que no hay nada que ver con eso. Utilice aserción de tipo en este caso.

@toiletpatrol en realidad, la solución de @decademoon (con mi ligera enmienda) maneja automáticamente los accesorios predeterminados muy bien. Definitivamente podría fusionarse con las definiciones de DT para React para proporcionar el estándar de características para todos.

@toiletpatrol @RobRendell lo viste https://github.com/Microsoft/TypeScript/issues/23812?

@vkrol Vi eso, pero puedo eliminar la implementación de decademoon en mi base de código ahora mismo sin esperar los lanzamientos de nuevas funciones.

Otra solución que estoy usando por ahora para casos complicados:

const restWithDefaults = { ...Component.defaultProps, ...rest };
return <Component {...restWithDefaults} />;

Supongo que no tiene nada de malo, así que lo dejo aquí como una solución sucia pero simple.

Las tipificaciones de TS 3.2 y react 16.7 están solucionando esto. podemos cerrar?

@Hotell , ¿cómo debería manejarse eventualmente? Todavía no puedo hacer que esto funcione correctamente

Para ahorrar tiempo a otros, aquí hay un enlace a las notas de la versión de Typescript 3:
Soporte para defaultProps en JSX

@cbergmiller Me temo que esas son las notas de lanzamiento de TypeScript 3.1 🙃

sigo teniendo el mismo problema con React.FunctionComponent

@denieler No recomendaría usar defaultProps con React.FunctionComponent , no es natural. Es mejor usar los parámetros de función predeterminados:

interface HelloProps {
  name?: string;
  surname?: string;
}

const HelloComponent: React.FunctionComponent<HelloProps> = ({
  name = 'John',
  surname = 'Smith',
}) => {
  return <div>Hello, {name} {surname}!</div>
};

@mgol ¿Cómo definiría los parámetros de función predeterminados si no quisiera desestructurar los accesorios?
Solo puedo pensar en desestructurar solo las propiedades "predeterminadas" de esta manera:

interface HelloProps {
  name?: string;
  surname?: string;
}

const HelloComponent: React.FunctionComponent<HelloProps> = ({
  name = 'John',
  surname = 'Smith',
  ...props
}) => {
  return <div>Hello, {name} {surname}! You are {props.age} years old.</div>
};

Pero me parece vergonzoso extraer solo algunos de los accesorios.

@glecetre Puedes usar:

HelloComponent.defaultProps = {
    name: 'John',
    surname: 'Smith'
}

@Glinkis , tenga en cuenta https://github.com/reactjs/rfcs/pull/107/files#diff -20b9b769068a185d90c23b58a2095a9dR184.

@glecetre ¿Por qué no quieres desestructurar todos los accesorios? Es más simple que definir defaultProps y más fácil de escribir. El tipo de accesorios del componente basado en clases puede molestarlo si exporta para usarlo externamente, ya que los accesorios que se requieren pueden no ser necesarios si hay una entrada para ellos en defaultProps . Usar defaultProps también parece un poco mágico mientras que en la desestructuración de parámetros todo es JavaScript.

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

Temas relacionados

jrmcdona picture jrmcdona  ·  3Comentarios

demisx picture demisx  ·  3Comentarios

alisabzevari picture alisabzevari  ·  3Comentarios

victor-guoyu picture victor-guoyu  ·  3Comentarios

csharpner picture csharpner  ·  3Comentarios