React Native - Компонент высшего порядка с обратным вызовом ref в TypeScript выдает ошибку компиляции - PullRequest
0 голосов
/ 26 сентября 2018

Intro

Я пытаюсь создать Higher Order Component для компонента Dialog в React Native.К сожалению, у меня есть некоторые ошибки компиляции, которые я вообще не понимаю.Я следовал этому уроку на Higher Order Components в TypeScript, но он не показывает пример того, как заставить ref работать.

Настройка

У меня естькомпонент с именем DialogLoading, и я экспортирую его через Higher Order Component с именем withActions.Компонент withActions определяет два интерфейса, чтобы определить, какие реквизиты он вводит и какие дополнительные реквизиты он принимает. В следующем коде параметры типа C, A и P обозначают ComponentType, ActionType и PropType соответственно.

Интерфейсы:

interface InjectedProps<A> 
{   onActionClicked: (action: A) => void;}

и

interface ExternalProps<C, A>
{   onActionClickListener?: (component: C | null, action: A) => void;}

Я также объявляю псевдоним типа, который обозначает окончательный тип реквизита для HOC.Этот тип должен иметь все реквизиты обернутого компонента, все реквизиты интерфейса ExternalProps<C, A>, но не реквизиты интерфейса InjectedProps<A>.Это объявляется следующим образом:

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

type HocProps<C, A, P extends InjectedProps<A>> = Subtract<P, InjectedProps<A>> & ExternalProps<C, A>;

Затем Higher Order Component объявляется следующим образом:

export default <C, A, P extends InjectedProps<A>> (WrappedComponent: React.ComponentType<P>) =>
{
    const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
    {
        ...Contents of class removed for breivity.

        private onActionClicked = (action: A) =>
        {
            this.onActionClickedListeners.forEach(listener => 
            {   listener(this.wrapped, action);});
        }

        private wrapped: C | null;

        render()
        {
            return (
                <WrappedComponent ref={i => this.wrapped = i} onActionClicked={this.onActionClicked} {...this.props} />
            );
        }
    }

    return hoc;
}

и может использоваться:

<DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;

Проблема

При обратном вызове ref внутри функции рендеринга HOC, TypeScript выдает следующие сообщения об ошибках:

[ts] Property 'ref' does not exist on type 'IntrinsicAttributes & InjectedProps<A> & { children?: ReactNode; }'.
[ts] Type 'Component<P, ComponentState, never> | null' is not assignable to type 'C | null'.
     Type 'Component<P, ComponentState, never>' is not assignable to type 'C'.

Я подозреваю, что это потому, что WrappedComponentпередается, имеет тип React.ComponentType<P>, который является типом объединения React.ComponentClass<P> и React.SFC<P>.Выдается ошибка, поскольку компоненты без сохранения состояния в React не принимают обратный вызов ref.Возможным решением было бы изменить его тип на просто React.ComponentClass<P>.

Этот вид устраняет проблему, но, как ни странно, новая ошибкатеперь добавляется onActionClicked опора обернутого компонента!Ошибка:

[ts] Type '(action: A) => void' is not assignable to type '(IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>)["onActionClicked"]'.
WithActions.tsx(7, 5): The expected type comes from property 'onActionClicked' which is declared here on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>'

Эта вторая ошибка полностью сбивает меня с толку.Что делает его еще более странным , так это то, что когда я настраиваю псевдоним типа для HocProps следующим образом (т.е. я больше не вычитаю InjectedProps<A> из P):

type HocProps<C, A, P extends InjectedProps<A>> = P & ExternalProps<C, A>;

Ошибка на onActionClicked удалена!Мне это кажется странным, потому что определение типа HocProps не имеет ничего общего с типом prop обернутого компонента!Это «решение», однако, для меня нежелательно, потому что теперь InjectedProps<A> также могут быть введены пользователем HOC.

Вопрос

Итак, куда я иду?здесь не так?

  • Правильно ли я считаю, что обратный вызов ref не работал, поскольку тип Wrapped Component был React.ComponentType<P> вместо React.ComponentClass<P>?

  • Почему изменение типа Wrapped Component на React.ComponentClass<P> приводит к ошибке компиляции на onActionClicked реквизите Wrapped Component?

  • Почему изменение псевдонима типа для HocProps удалить указанную ошибку на onActionClicked проп?Разве они совершенно не связаны?

  • Правильно ли настроена функция Subtract, которую я приготовил?Это то, откуда приходят ошибки?

Любая помощь будет принята с благодарностью, поэтому спасибо заранее!

1 Ответ

0 голосов
/ 27 сентября 2018

Правильно ли я считаю, что обратный вызов ref не работал, потому что тип Wrapped Component был React.ComponentType<P> вместо React.ComponentClass<P>?

Грубо говоря, да.Когда вы создаете элемент JSX из WrappedComponent, который имеет тип объединения (React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P>), TypeScript находит тип реквизита, соответствующий каждой альтернативе объединения, а затем принимает объединение типов реквизитов, используя сокращение подтипа.Из checker.ts (который слишком велик для ссылки на строки в GitHub):

    function resolveCustomJsxElementAttributesType(openingLikeElement: JsxOpeningLikeElement,
        shouldIncludeAllStatelessAttributesType: boolean,
        elementType: Type,
        elementClassType?: Type): Type {

        if (elementType.flags & TypeFlags.Union) {
            const types = (elementType as UnionType).types;
            return getUnionType(types.map(type => {
                return resolveCustomJsxElementAttributesType(openingLikeElement, shouldIncludeAllStatelessAttributesType, type, elementClassType);
            }), UnionReduction.Subtype);
        }

Я не уверен, почему это правило;пересечение будет иметь больше смысла для обеспечения присутствия всех необходимых реквизитов независимо от того, какой альтернативой является объединение компонента.В нашем примере тип реквизита для React.ComponentClass<P> включает ref, а тип реквизита для React.StatelessComponent<P> - нет.Обычно свойство считается «известным» для типа объединения, если оно присутствует хотя бы в одном компоненте объединения.Однако в этом примере сокращение подтипа выбрасывает тип реквизита для React.ComponentClass<P>, поскольку это подтип типа (имеет больше свойств, чем) типа реквизита для React.StatelessComponent<P>, поэтому у нас остается только React.StatelessComponent<P>, у которого нет свойства ref.Опять же, все это кажется странным, но выдает ошибку, которая указывает на фактическую ошибку в вашем коде, поэтому я не склонен сообщать об ошибке в TypeScript.

Почему меняется Wrapped Component введите React.ComponentClass<P>, что приведет к ошибке компиляции onActionClicked реквизита Wrapped Component?

Основная причина этой ошибки заключается в том, что TypeScript не может объяснить, что комбинация onActionClicked={this.onActionClicked} {...this.props} типа Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; } обеспечивает необходимый тип реквизита P.Ваше намерение состоит в том, что если вы вычтете onActionClicked из P и затем добавите его обратно, у вас останется P, но в TypeScript нет встроенного правила для проверки этого.(Существует потенциальная проблема, заключающаяся в том, что P может объявить свойство onActionClicked, тип которого является подтипом (action: A) => void, но ваш шаблон использования достаточно распространен, и я ожидаю, что если TypeScript добавит такое правило, правило будетКак-то взломать эту проблему.)

Заблуждение, что TypeScript 3.0.3 сообщает об ошибке на onActionClicked (хотя это может быть связано с упомянутой мной проблемой).Я протестировал, и в какой-то момент между 3.0.3 и 3.2.0-dev.20180926 поведение изменилось, чтобы сообщить об ошибке на WrappedComponent, что кажется более разумным, поэтому здесь дальнейших действий не требуется.

Причина, по которой ошибка не возникает, когда тип WrappedComponent равен React.ComponentType<P>, заключается в том, что для компонента функции без сохранения состояния (в отличие от класса компонента) TypeScript проверяет только то, что вы пропустили достаточное количество реквизитов для удовлетворенияограничение типа реквизита P, то есть InjectedProps<A>, а не P.Я считаю, что это ошибка, и сообщил о ней .

Почему изменение псевдонима типа для HocProps удаляет указанную ошибку на onActionClicked проп?Разве они совершенно не связаны?

Потому что тогда {...this.props} сам по себе удовлетворяет требуемым P.

Является ли функциональность Subtract, которую я приготовил?правильный?Это то, откуда приходят ошибки?

Ваш Subtract правильный, но, как описано выше, TypeScript очень мало поддерживает рассуждения о базовых Pick и Exclude.

Чтобы решить исходную проблему, я рекомендую использовать псевдонимы и пересечения типов вместо вычитаний, как описано в этого ответа .В вашем случае это будет выглядеть так:

import * as React from "react";

interface InjectedProps<A> 
{   onActionClicked: (action: A) => void;}

interface ExternalProps<C, A>
{   onActionClickListener?: (component: C | null, action: A) => void;}

// See https://stackoverflow.com/a/52528669 for full explanation.
const hocInnerPropsMarker = Symbol();
type HocInnerProps<P, A> = P & {[hocInnerPropsMarker]?: undefined} & InjectedProps<A>;

type HocProps<C, A, P> = P & ExternalProps<C, A>;

const hoc = <C extends React.Component<HocInnerProps<P, A>>, A, P>
    (WrappedComponent: {new(props: HocInnerProps<P, A>, context?: any): C}) =>
{
    const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
    {
        onActionClickedListeners;  // dummy declaration

        private onActionClicked = (action: A) =>
        {
            this.onActionClickedListeners.forEach(listener => 
            {   listener(this.wrapped, action);});
        }

        private wrapped: C | null;

        render()
        {
            // Workaround for https://github.com/Microsoft/TypeScript/issues/27484
            let passthroughProps: Readonly<P> = this.props;
            let innerProps: Readonly<HocInnerProps<P, A>> = Object.assign(
                {} as {[hocInnerPropsMarker]?: undefined},
                passthroughProps, {onActionClicked: this.onActionClicked});
            return (
                <WrappedComponent ref={i => this.wrapped = i} {...innerProps} />
            );
        }
    }

    return hoc;
}

interface DiagLoadingOwnProps {
    title: string;
    section: string;
}

// Comment out the `{[hocInnerPropsMarker]?: undefined} &` in `HocInnerProps`
// and uncomment the following two lines to see the inference mysteriously fail.

//type Oops1<T> = DiagLoadingOwnProps & InjectedProps<string>;
//type Oops2 = Oops1<number>;

class DiagLoadingOrig extends React.Component<
    // I believe that the `A` type parameter is foiling the inference rule that
    // throws out matching constituents from unions or intersections, so we are
    // left to rely on the rule that matches up unions or intersections that are
    // tagged as references to the same type alias.
    HocInnerProps<DiagLoadingOwnProps, string>,
    {}> {}
const DialogLoading = hoc(DiagLoadingOrig);

class OtherComponent extends React.Component<{}, {}> {
    onActionClickListener;
    render() {
        return <DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;
    }
}

(Здесь я также изменил тип WrappedComponent, чтобы его тип экземпляра был C, чтобы присвоение this.wrapped type-чеки.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...