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;
