Правильно ли я считаю, что обратный вызов 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-чеки.)