Pesquisa de site

reactjs - Solução alternativa em TypeScript para props restantes no React


Atualizado para TypeScript 2.1

O TypeScript 2.1 agora suporta spread/rest de objetos, então não são mais necessárias soluções alternativas!

Pergunta Original

O TypeScript suporta atributos spread em JSX, que são comumente usados no React para passar atributos HTML de um componente para um elemento HTML renderizado:

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" />

No entanto, o React introduziu um aviso se você passar quaisquer props desconhecidas para um elemento HTML. O exemplo acima produziria um aviso em tempo de execução do React indicando que textToDisplay é uma prop desconhecida de <a>. A solução sugerida para um caso como este exemplo é usar propriedades de resto de objeto para extrair suas props personalizadas e usar o restante para os atributos espalhados no JSX:

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

Mas o TypeScript ainda não suporta essa sintaxe. Eu sei que, esperançosamente, algum dia seremos capazes de fazer isso no TypeScript. (Atualização: o TS 2.1 agora suporta spread/rest de objetos! Por que você ainda está lendo isso??) Enquanto isso, quais são algumas soluções alternativas? Estou procurando uma solução que não comprometa a segurança de tipos e estou achando isso surpreendentemente difícil. Por exemplo, eu poderia fazer isso:

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

Mas isso requer o uso de nomes de propriedades como strings que não são validados contra as props reais e, portanto, são propensos a erros de digitação e a um suporte ruim do IDE. Existe uma maneira melhor de fazermos isso?

Resposta #1

Na verdade, é mais fácil do que todas as respostas acima. Você só precisa seguir o exemplo abaixo:

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
}

Resposta #2

Provavelmente você não consegue evitar criar um novo objeto com um subconjunto das propriedades de this.props, mas você pode fazer isso com segurança de tipos.

Por exemplo:

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

Usar o objeto LinkPropsKeys, que precisa corresponder ao LinkProps, ajudará você a manter as chaves entre a interface e a pesquisa em tempo de execução sincronizadas.

Resposta #3

use ...rest

type ButtonProps = {
    disabled: boolean;
};

function Button(props: ButtonProps): JSX.Element {
    const {disabled = false, ...rest} = props;
...
return (
    <button disabled={disabled} {...rest}>
....

Resposta #4

Para aqueles que podem não entender rapidamente o que [x:string]: any; na resposta aceita faz: Embora seja muito parecido com a sintaxe de arrays, na verdade está especificando um objeto cujas chaves são do tipo string e seus valores são do tipo any. É chamado de "Assinatura de Índice" na terminologia do TypeScript.

No entanto, também note que, às vezes, como uma alternativa e solução menos frouxa em tipos, uma biblioteca que você está usando pode ter tipos exportados também, então você poderia usar esses.

Por exemplo, ao estender os Botões do Ant, pode-se fazer assim:

import { ReactNode } from "react";
import { Button as AntButton } from "antd";
import { NativeButtonProps } from "antd/lib/button/button";

interface IButtonProps {
  children?: ReactNode;
}

const Button = ({
  children,
  ...rest
}: IButtonProps & NativeButtonProps): JSX.Element => {
  return <AntButton {...rest}>{children}</AntButton>;
};

export default Button;

NOTA1: O operador & (&) em IButtonProps & NativeButtonProps simplesmente faz a "junção" de tipos no TypeScript. Agora você não perde a intellisense para as props do Ant Button no seu próprio Button, porque você não usa mais any. Os tipos do Ant Button e o seu IButtonProps são combinados e, portanto, existem ambos.

NOTA2: Você também pode se perguntar onde encontrei esse tipo. Este tipo foi exportado aqui: https://github.com/ant-design/ant-design/blob/master/components/button/button.tsx#L124 E o caminho de inclusão dele também pode ser percebido usando o Intellisense, basta começar a digitar NativeButton... e ele deve ser sugerido para você.

Resposta #5

React.HtmlAttributes no exemplo acima agora é genérico, então eu precisava estender de React.AnchorHTMLAttributes<HTMLAnchorElement>.

Exemplo :

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;

Artigos relacionados