web-dev-qa-db-fra.com

Solution de contournement TypeScript pour les accessoires de repos dans React

Mis à jour pour TypeScript 2.1

TypeScript 2.1 prend désormais en charge la répartition/repos des objets , donc aucune solution de contournement n'est plus nécessaire!


Question d'origine

TypeScript prend en charge attributs de diffusion JSX qui est couramment utilisé dans React pour passer des attributs HTML d'un composant à un élément HTML rendu:

interface LinkProps extends React.HTMLAttributes {
  textToDisplay: string;
}

class Link extends React.Component<LinkProps, {}> {
  public render():JSX.Element {
    return (
      <a {...this.props}>{this.props.textToDisplay}</a>
    );
  }
}

<Link textToDisplay="Search" href="http://google.com" />

Cependant, React a introduit un avertissement si vous passez des accessoires inconnus à un élément HTML . L'exemple ci-dessus produirait un React runtime avertissement que textToDisplay est un accessoire inconnu de <a>. La solution suggérée pour un cas comme cet exemple est d'utiliser propriétés de repos d'objet pour extraire vos accessoires personnalisés et utiliser le reste pour les attributs de propagation JSX:

const {textToDisplay, ...htmlProps} = this.props;
return (
  <a {...htmlProps}>{textToDisplay}</a>
);

Mais TypeScript ne prend pas encore en charge cette syntaxe. Je sais que j'espère un jour nous pourrons le faire en TypeScript . ( Mettre à jour: TS 2.1 prend désormais en charge la répartition/repos des objets ! Pourquoi lisez-vous toujours ceci ??) En attendant, quelles sont quelques solutions? Je recherche une solution qui ne compromet pas la sécurité des caractères et la trouve étonnamment difficile. Par exemple, je pourrais faire ceci:

const customProps = ["textDoDisplay", "otherCustomProp", "etc"];
const htmlProps:HTMLAttributes = Object.assign({}, this.props);
customProps.forEach(prop => delete htmlProps[prop]);

Mais cela nécessite l'utilisation de noms de propriétés de chaîne qui ne sont pas validés par rapport aux accessoires réels et donc sujets aux fautes de frappe et aux mauvais IDE support. Y a-t-il une meilleure façon de le faire?

22
Aaron Beall

Vous ne pouvez probablement pas éviter de créer un nouvel objet avec un sous-ensemble des propriétés de this.props, mais vous pouvez le faire avec la sécurité de type.

Par exemple:

interface LinkProps {
    textToDisplay: string;
}

const LinkPropsKeys: LinkProps = { textToDisplay: "" };

class Link extends React.Component<LinkProps & React.HTMLAttributes, {}> {
    public render(): JSX.Element {
        return (
            <a { ...this.getHtmlProps() }>{ this.props.textToDisplay }</a>
        );
    }

    private getHtmlProps(): React.HTMLAttributes {
        let htmlProps = {} as React.HTMLAttributes;

        for (let key in this.props) {
            if (!(LinkPropsKeys as any)[key]) {
                htmlProps[key] = this.props[key];
            }
        }

        return htmlProps;
    }
}

L'utilisation de l'objet LinkPropsKeys, qui doit correspondre à LinkProps, vous aidera à garder les clés entre l'interface et la recherche d'exécution synchronisées.

6
Nitzan Tomer

C'est en fait plus facile que toutes les réponses ci-dessus. Il vous suffit de suivre l'exemple ci-dessous:

type Props = {
  id: number,
  name: string;
  // All other props
  [x:string]: any;
}

const MyComponent:React.FC<Props> = props => {
  // Any property passed to the component will be accessible here
}

J'espère que cela t'aides.

5
Willy

React.HtmlAttributes dans l'exemple ci-dessus est maintenant générique, donc je devais étendre de React.AnchorHTMLAttributes<HTMLAnchorElement>.

Exemple:

import React from 'react';

type  AClickEvent = React.MouseEvent<HTMLAnchorElement>;

interface LinkPropTypes extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
    to: string;
    onClick?: (x: AClickEvent) => void;
}

class Link extends React.Component<LinkPropTypes> {
  public static defaultProps: LinkPropTypes = {
    to: '',
    onClick: null,
  };

private handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
   ...
    event.preventDefault();
    history.Push(this.props.to);
 };

  public render() {
    const { to, children, ...props } = this.props;
    return (
      <a href={to} {...props} onClick={this.handleClick}>
        {children}
      </a>
    );
    }
}

export default Link;
3
Damian Green

J'ai accepté la réponse de Nitzen Tomer parce que c'était l'idée de base que j'allais chercher.

En tant que solution plus généralisée, c'est ce que j'ai fini par choisir:

export function rest(object: any, remove: {[key: string]: any}) {
  let rest = Object.assign({}, object);
  Object.keys(remove).forEach(key => delete rest[key]);
  return rest;
}

Je peux donc l'utiliser comme ceci:

const {a, b, c} = props;
const htmlProps = rest(props, {a, b, c});

Et une fois que TypeScript prend en charge le repos/propagation d'objet, je peux simplement rechercher toutes les utilisations de rest() et le simplifier en const {a, b, c, ...htmlProps} = props.

1
Aaron Beall

Un getter comme celui-ci pourrait fonctionner:

class Link extends React.Component<{
  textToDisplay: string;
} & React.HTMLAttributes<HTMLDivElement>> {

  static propTypes = {
    textToDisplay: PropTypes.string;
  }

  private get HtmlProps(): React.HTMLAttributes<HTMLAnchorElement> {
    return Object.fromEntries(
      Object.entries(this.props)
      .filter(([key]) => !Object.keys(Link.propTypes).includes(key))
    );
  }

  public render():JSX.Element {
    return (
      <a {...this.HtmlProps}>
        {this.props.textToDisplay}
      </a>
    );
  }
}

<Link textToDisplay="Search" href="http://google.com" />
1
Okku