TypeScript обходной путь для реквизита отдыха в реактиве

Обновлено для TypeScript 2.1

TypeScript 2.1 теперь поддерживает распространение/отдых объектов, поэтому обходных путей больше не требуется!


Оригинальный вопрос

TypeScript поддерживает атрибуты распространения JSX, которые обычно используются в React для передачи атрибутов HTML из компонента в визуализированный элемент HTML:

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="#" onclick="location.href='http://google.com'; return false;" />

Однако React ввел предупреждение , если вы передаете какой-либо неизвестный реквизит элементу HTML. Приведенный выше пример выдаст предупреждение времени выполнения React о том, что textToDisplay является неизвестной опорой <a>. Предлагаемое решение для случая, подобного этому, состоит в том, чтобы использовать свойства покоя объекта для извлечения ваших пользовательских реквизитов и использовать остальное для атрибутов распространения JSX:

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

Но TypeScript пока не поддерживает этот синтаксис. Я знаю, что, надеюсь, когда-нибудь мы сможем сделать это в TypeScript. (Обновление: TS 2.1 теперь поддерживает распространение/отдых объектов! Почему ты все еще читаешь это?) А пока какие обходные пути есть? Я ищу решение, которое не ставит под угрозу безопасность типов, и нахожу его на удивление трудным. Например, я мог бы сделать это:

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

Но для этого необходимо использовать имена строковых свойств, которые не проверяются на соответствие фактическим реквизитам и поэтому подвержены опечаткам и плохой поддержке IDE. Есть ли лучший способ сделать это?

Ответ 1

Вероятно, вам не удастся создать новый объект с подмножеством свойств this.props, но вы можете сделать это с безопасностью типа.

Например:

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

Использование объекта LinkPropsKeys, который должен соответствовать LinkProps, поможет вам синхронизировать ключи между интерфейсом и синхронизацией времени выполнения.

Ответ 2

React.HtmlAttributes в приведенном выше примере теперь является общим, поэтому мне нужно было перейти от React.AnchorHTMLAttributes<HTMLAnchorElement>.

Пример:

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

Я принял Nitzen Tomer, потому что это была основная идея, по которой я собирался.

В качестве более обобщенного решения это то, с чем я столкнулся:

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

Поэтому я могу использовать его следующим образом:

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

И как только TypeScript поддерживает объект rest/spread, я могу просто искать все применения rest() и упростить его до const {a, b, c, ...htmlProps} = props.

Ответ 4

Такой геттер может сработать:

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

Ответ 5

Это на самом деле проще, чем все ответы выше. Вам просто нужно следовать примеру ниже:

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
}

Надеюсь, это поможет.