Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compose]: Fix type for createHigherOrderComponent #40984

Closed
wants to merge 5 commits into from

Conversation

dmsnell
Copy link
Member

@dmsnell dmsnell commented May 11, 2022

What?

When refactoring createHigherOrderComponent in #37795
an incorrect type was introduced in an attempt to correct
the previous type. The fix in that patch was focused on
the narrow use-case of a prop-injecting higher-order component.

In this patch we're lifting the type signature into the function
which calls createHigherOrderComponent. There's a limitiation
I'm not sure how to get past when adding the type parameters to
the generalized interface; that is, the function produced by
createHigherOrderComponent needs its own type parameters for
input and output specified by the given mapComponent mapper.

This change require more type juggling when defining a new
higher-order component but removes the arbitrary constraint
on what props may be passed into or come out of the higher-order
component.

Why?

The type in createHigherOrderComponent was overly-restrictive.

How?

Moves the type parameters from createHigherOrderComponent to
the code where it's called.

Testing Instructions

As a type-only change there should be no impact on the built artifacts.

No tests should start failing.
Review the approach taken for the type of createHigherOrderComponent
as well as how this demands a change in the way calling code is
typed.

Note that if no type is provided at the call-site then the output
of createHigherOrderComponent will be ComponentType<any>, which
still should be valid even if it doesn't track the appropriate Props.

const withAnything = createHigherOrderComponent( x => x, 'anything' );

const Widget = (props: WidgetProps) => 
const AnyWidget = withAnything(Widget);
//    ^? ComponentType<any>

The proper way to handle this is to look at the examples and type the
variable declaration itself.

const withLimit: <
	Inner extends ComponentType<any>
>(limit: number) => (Inner: Inner) => ComponentType<PropsOf<Inner>> =
	limit => createHigherOrderComponent( Inner => props =>
		props.limit < limit ? <Inner {...props} /> : void
	);

const LimitedWidget = withLimit(Widget);
//    ^? ComponentType<WidgetProps>

@dmsnell dmsnell requested a review from ajitbohra as a code owner May 11, 2022 01:19
@dmsnell
Copy link
Member Author

dmsnell commented May 11, 2022

After a lot of trial and error I was unable to get to where I wanted to be but I think there's an issue with the fact that the type parameters can't be part of createHigherOrderComponent though I don't fully understand why.

If you move some of the type signatures to the inside of createHigherOrderComponent and let MapCoponent be a type-parameter of that wrapper then there are all sorts of complaints about satisfying the type and about the React types. If changing from ComponentType<any> to Props, ComponentType<Props> there are errors about React children.

Would love some help figuring this one out. Turns out that as @jsnajdr pointed out, there are almost no actual restrictions on the final result from a higher-order component and the restrictions come from the implementation inside of it. For example, one higher-order component may want to be the sole-supplier of a prop and therefore forbid passing it down to the wrapped component (see withInstanceId) while another may only provide that prop if it's not externally provided (see withColorContext).

The type changes here more-closely reflect that I think, and are closer to what I think is right than we had in #37795 or its parent.

cc: @sirbrillig

@gziolo gziolo added [Type] Code Quality Issues or PRs that relate to code quality [Package] Compose /packages/compose labels May 11, 2022
@sarayourfriend
Copy link
Contributor

sarayourfriend commented May 11, 2022

These types still seem less correct or comprehensive than the ones I originally wrote. For example, with the original types, you get this error with the ifCondition HOC:

image

With the previous changes and these ones you don't get any errors like that.

Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmsnell I think the main blocker of this work is that we don't have clear shared requirements on how the createHigherOrderComponent should behave.

When I read your comments like # or # I vaguely understand what issues you're describing, but I'm not really sure. When trying to type HOCs myself, I also run into some issues, but are we really talking about the same thing? Don't know.

What if we created a types unit test file, like test/types.tsx, and put usage examples there? Like "I want to write a HOC like this and expect the types to be X and Y" or "when I write this I expect that the compiler reports an error."

We're done when the unit test file passes typechecking without errors, and with @ts-expect-error we can even check there are errors where we want them to be.

Our expectations would be written down as code, and I believe that would clarify the collaboration a lot.

infer Props
>
? Props
: never;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a React.ComponentProps<C> type that extracts the props type from a component type, could we use that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you. I swapped to ComponentProps in cfa07f3

had looked for it but didn't find that when I initially wrote the patch.

export function createHigherOrderComponent(
mapComponent: ( Inner: ComponentType< any > ) => ComponentType< any >,
modifierName: string
): typeof mapComponent {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now when createHigherOrderComponent and mapComponent are no longer generic functions, with type parameters, that means that createHigherOrderComponent erases the type of mapComponent and returns a generic any => any mapper.

For example, if I pass ComponentType<MyProps> to pure, I should get a ComponentType<MyProps>, component of the same type. But I get ComponentType<any>.

@jsnajdr
Copy link
Member

jsnajdr commented May 12, 2022

In #37795 (comment) @dmsnell writes:

There may still be a way to revert to that while removing the additional type wrappers like PropInjectingHigherOrderComponent that might have muddied the water.

and I'm now starting to understand the motivation for #37795 because I found that the PropInjectingHigherOrderComponent type was wrong. It was defined as:

type PropInjectingHigherOrderComponent<TRemovedProps> =
  <TProps extends TRemovedProps>( Inner: ComponentType< TProps > ) =>
    ComponentType< Omit< TProps, keyof TRemovedProps > >;

The idea is that, for example, a withInstanceId HOC wants to inject an instanceId prop, typed as:

type InstanceIdProps = { instanceId: string | number };

and that means the type of withInstanceId is PropInjectingHigherOrderComponent<InstanceIdProps>. It takes any component that accepts an instanceId prop (written as TProps extends InstanceIdProps) and returns a component that no longer accepts instanceId and accepts only Omit<TProps, 'instanceId'>.

But there's subtle bug. Consider this example:

type InstanceIdProps = { instanceId: string | number };
const withInstanceId: PropInjectingHigherOrderComponent<InstanceIdProps> = ...;

type FooProps = { foo: string, instanceId: string };
function Foo({ foo, instanceId }: FooProps) {
  return <div id={instanceId.slice(0,4)}>{foo}</div>;
}
const WrappedFoo = withInstanceId(Foo);

TypeScript will happily accept this code, but it has a serious type error and can crash at runtime! Can you spot it?

The Foo component only accepts a string as instanceId, and calls .slice on the string. But the withInstanceId HOC can also inject a number value! The type clearly says that but TypeScript doesn't complain at all.

Wasn't the FooProps extends InstanceIdProps constraint supposed to catch that? The FooProps type is obviously not compatible with InstanceIdProps.

But extends checks for a different kind of compatibility. A extends B means "values of type A are assignable to variables of type B." And in this sense, FooProps and InstanceIdProps are compatible:

const fooProps: FooProps = { foo: 'hello', instanceId: 'world' };
const idProps: InstanceIdProps = fooProps;

This assignment is perfectly fine because FooProps.instanceId is always a string and can be assigned to string | number.

But our HOC does assignments in the opposite direction, taking value of InstanceIdProps and assigning part of it to FooProps. We need to express the relationship between TProps and TRemovedProps differently.

@jsnajdr
Copy link
Member

jsnajdr commented May 12, 2022

But our HOC does assignments in the opposite direction, taking value of InstanceIdProps and assigning part of it to FooProps. We need to express the relationship between TProps and TRemovedProps differently.

I found that react-redux uses the following two tricks to type connect:

First, there is a Matching<Injected, Original> type that merges the two types, giving precedence to fields in Injected:

Matching<{ id: string }, { id: number, foo: string }> = { id: string, foo: string }

Second, instead of Props1 extends Props2 constraint they check ComponentType<Props1> extends ComponentType<Props2>. Because ComponentType<T> is contravariant, it holds when Props2 extends Props1.

Combining the two ideas together, we get:

type InjectorHOC<Injected> =
  <C extends ComponentType<Matching<Injected,ComponentProps<C>>(component: C) =>
    ComponentType<Omit<ComponentProps<C>, keyof Injected>;

Now InjectorHOC<{ instanceId: string | number }> is a function that accepts only component that have instanceId: string | number as a prop, and returns a component without instanceId prop.

Hope this is helpful and moves us forward. For today, the TypeScript center in my brain is totally burned out and I'm moving on to something else 🙂

When refactoring `createHigherOrderComponent` in #37795
an incorrect type was introduced in an attempt to correct
the previous type. The fix in that patch was focused on
the narrow use-case of a _prop-injecting higher-order component_.

In this patch we're lifting the type signature into the function
which calls `createHigherOrderComponent`. There's a limitiation
I'm not sure how to get past when adding the type parameters to
the generalized interface; that is, the function _produced_ by
`createHigherOrderComponent` needs its own type parameters for
input and output specified by the given `mapComponent` mapper.

This change require more type juggling when defining a new
higher-order component but removes the arbitrary constraint
on what props may be passed into or come out of the higher-order
component.
@dmsnell dmsnell force-pushed the types/fix-create-higher-order-component branch from 32536ad to 8040756 Compare May 12, 2022 23:07
@dmsnell
Copy link
Member Author

dmsnell commented May 13, 2022

@dmsnell I think the main blocker of this work is that we don't have clear shared requirements on how the createHigherOrderComponent should behave.

@jsnajdr yes! I think that's exactly right, and truth be told the more I look into it the less power I see createHigherOrderComponent holding in order to enforce that. I've called into question the value of the utility function in any case because all it's doing is adding the display name to the generated inner component and complicating the types.

export const createHigherOrderComponent = ( mapper, name ) => ( Inner ) => {
	const Outer = mapper( Inner );
	Outer.displayName = hocName( name, Inner );

	return Outer;
};

const hocName = ( name: string, Inner: ComponentType< any > ): string => {
	const inner = Inner.displayName || Inner.name || 'Component';
	const outer = upperFirst( camelCase( name ) );

	return `${ outer }(${ inner })`;
};

When I was first trying to understand this function I was very confused. mapComponent is really the higher-order component and this is more a higher-order higher-order component, making the types even harder because we have to specify a type parameter for Inner even though that's a property of mapComponent more than it is of createHigherOrderComponent.

Do you think we could roll back the types on this and leave it untyped for now? Every time I try and capture and preserve the type of mapComponent I hit dead-ends and confusing errors at the call-sites.

In the meantime I could see us extracting hocName and using that directly in the "real" HOCs (like ifCondition) instead of wrapping with createHigherOrderComponet. That leaves fewer wrappings and easier types. We could even consider deprecating createHigherOrderComponent and leave it untyped until we remove it.

@jsnajdr
Copy link
Member

jsnajdr commented May 13, 2022

I've called into question the value of the utility function in any case because all it's doing is adding the display name to the generated inner component and complicating the types.

The createHigherOrderComponent helper indeed only adds the displayName field and nothing else. But that should make it easy to type, not difficult, right? It should just blindly return a wrapper function that has the same type as the mapComponent type. And enforce just two constraints on mapComponent:

  1. mapComponent is a function with one parameter.
  2. The types of the parameter and of the return value must be something that has a .displayName field, and the parameter also needs to have a .name field. ComponentType<any> satisfies that.

I think it's useful to divide the problem into two separate ones:

  1. Typing the mapComponent itself. That's where we encounter the HOC<Inner, Outer> and PropInjectingHOC<InjectedProps> types. This has nothing to do with createHigherOrderComponent.
  2. Make sure that createHigherOrderComponent( mapComponent ) is always able to determine the type of mapComponent and infer its own type parameters from it. This sounds trivial, but for some reason it doesn't always work. I'd like to understand better why that happens.

@dmsnell
Copy link
Member Author

dmsnell commented May 13, 2022

@jsnajdr I haven't yet dug into the exclusionary types you suggested, but thank you for posting those as I think we'll need them.

In 0208be8 I have tried again to infer the type of mapComponent and let it pass through. Yesterday I thought we couldn't do this, but in running through your suggestion and the code again it seems to work.

Each actual higher-order component is free to define its own constraints and createHigherOrderComponent() doesn't impose anything other than mapComponent needs to take and produce a component.

@sarayourfriend I can't see enough in your screenshot to know what's going on or what you expect there, but you might check again. In this iteration we show∂ be able to fully-define in each HOC what they want, so we can dig in.

The plan is to get to the Matching<> stuff but I need a break.

@github-actions
Copy link

github-actions bot commented May 13, 2022

Size Change: +1.58 kB (0%)

Total Size: 1.24 MB

Filename Size Change
build/block-library/index.min.js 180 kB +648 B (0%)
build/compose/index.min.js 11.7 kB +7 B (0%)
build/customize-widgets/index.min.js 11.2 kB +169 B (+2%)
build/edit-navigation/index.min.js 16 kB +184 B (+1%)
build/edit-post/index.min.js 30.4 kB +156 B (+1%)
build/edit-site/index.min.js 47.6 kB +177 B (0%)
build/edit-widgets/index.min.js 16.4 kB +167 B (+1%)
build/preferences-persistence/index.min.js 2.23 kB +74 B (+3%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/annotations/index.min.js 2.77 kB
build/api-fetch/index.min.js 2.27 kB
build/autop/index.min.js 2.15 kB
build/blob/index.min.js 487 B
build/block-directory/index.min.js 6.51 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/index.min.js 150 kB
build/block-editor/style-rtl.css 14.9 kB
build/block-editor/style.css 14.9 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 59 B
build/block-library/blocks/avatar/style.css 59 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 445 B
build/block-library/blocks/button/editor.css 445 B
build/block-library/blocks/button/style-rtl.css 560 B
build/block-library/blocks/button/style.css 560 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 127 B
build/block-library/blocks/comment-template/style.css 127 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 95 B
build/block-library/blocks/comments/editor.css 95 B
build/block-library/blocks/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.53 kB
build/block-library/blocks/cover/style.css 1.53 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 417 B
build/block-library/blocks/embed/style.css 417 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 353 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 961 B
build/block-library/blocks/gallery/editor.css 964 B
build/block-library/blocks/gallery/style-rtl.css 1.51 kB
build/block-library/blocks/gallery/style.css 1.51 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 333 B
build/block-library/blocks/group/editor.css 333 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 731 B
build/block-library/blocks/image/editor.css 730 B
build/block-library/blocks/image/style-rtl.css 529 B
build/block-library/blocks/image/style.css 535 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 463 B
build/block-library/blocks/latest-posts/style.css 462 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 708 B
build/block-library/blocks/navigation-link/editor.css 706 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation-submenu/view.min.js 375 B
build/block-library/blocks/navigation/editor-rtl.css 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.95 kB
build/block-library/blocks/navigation/style.css 1.94 kB
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB
build/block-library/blocks/navigation/view.min.js 395 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 260 B
build/block-library/blocks/paragraph/style.css 260 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 69 B
build/block-library/blocks/post-comments-form/editor.css 69 B
build/block-library/blocks/post-comments-form/style-rtl.css 495 B
build/block-library/blocks/post-comments-form/style.css 495 B
build/block-library/blocks/post-comments/editor-rtl.css 77 B
build/block-library/blocks/post-comments/editor.css 77 B
build/block-library/blocks/post-comments/style-rtl.css 628 B
build/block-library/blocks/post-comments/style.css 628 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 721 B
build/block-library/blocks/post-featured-image/editor.css 721 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 323 B
build/block-library/blocks/post-template/style.css 323 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 370 B
build/block-library/blocks/pullquote/style.css 370 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 369 B
build/block-library/blocks/query/editor.css 369 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 397 B
build/block-library/blocks/search/style.css 398 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 759 B
build/block-library/blocks/site-logo/editor.css 759 B
build/block-library/blocks/site-logo/style-rtl.css 181 B
build/block-library/blocks/site-logo/style.css 181 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.37 kB
build/block-library/blocks/social-links/style.css 1.36 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 504 B
build/block-library/blocks/table/editor.css 504 B
build/block-library/blocks/table/style-rtl.css 625 B
build/block-library/blocks/table/style.css 625 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 149 B
build/block-library/blocks/template-part/editor.css 149 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 571 B
build/block-library/blocks/video/editor.css 572 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 993 B
build/block-library/common.css 990 B
build/block-library/editor-rtl.css 10.3 kB
build/block-library/editor.css 10.3 kB
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 11.5 kB
build/block-library/style.css 11.6 kB
build/block-library/theme-rtl.css 689 B
build/block-library/theme.css 694 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 47 kB
build/components/index.min.js 227 kB
build/components/style-rtl.css 15 kB
build/components/style.css 15 kB
build/core-data/index.min.js 14.6 kB
build/customize-widgets/style-rtl.css 1.39 kB
build/customize-widgets/style.css 1.39 kB
build/data-controls/index.min.js 663 B
build/data/index.min.js 7.98 kB
build/date/index.min.js 32 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.69 kB
build/edit-navigation/style-rtl.css 4.05 kB
build/edit-navigation/style.css 4.05 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/style-rtl.css 7.08 kB
build/edit-post/style.css 7.08 kB
build/edit-site/style-rtl.css 7.96 kB
build/edit-site/style.css 7.95 kB
build/edit-widgets/style-rtl.css 4.41 kB
build/edit-widgets/style.css 4.4 kB
build/editor/index.min.js 38.4 kB
build/editor/style-rtl.css 3.71 kB
build/editor/style.css 3.71 kB
build/element/index.min.js 4.3 kB
build/escape-html/index.min.js 548 B
build/format-library/index.min.js 6.62 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.66 kB
build/html-entities/index.min.js 454 B
build/i18n/index.min.js 3.79 kB
build/is-shallow-equal/index.min.js 535 B
build/keyboard-shortcuts/index.min.js 1.83 kB
build/keycodes/index.min.js 1.41 kB
build/list-reusable-blocks/index.min.js 1.75 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.94 kB
build/notices/index.min.js 957 B
build/nux/index.min.js 2.1 kB
build/nux/style-rtl.css 751 B
build/nux/style.css 749 B
build/plugins/index.min.js 1.98 kB
build/preferences/index.min.js 1.32 kB
build/primitives/index.min.js 949 B
build/priority-queue/index.min.js 628 B
build/react-i18n/index.min.js 704 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.69 kB
build/reusable-blocks/index.min.js 2.24 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11.2 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.52 kB
build/token-list/index.min.js 668 B
build/url/index.min.js 1.99 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 280 B
build/widgets/index.min.js 7.21 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.07 kB

compressed-size-action

@sarayourfriend
Copy link
Contributor

Sorry, the screenshot was bad. I've tried the same code on the latest version and it still does not catch the type error.

This is the code I'm using to exercise ifCondition:

const Comp = ({ foo }: { foo: string }) => <div>{foo}</div>

const ifBar = ifCondition((p: { bar: string }) => !!p.bar)
const IffComp = ifBar(Comp)

calling ifBar with a component that does not accept bar should be an error, unless ifCondition merged the predicate props with the props of the component. As it stands it causes IffComp to be ComponentType<any>.

The original version errored if you passed a component to ifBar that didn't accept the props used by the predicate.

Neither of these is actually correct, now that I think about it, but it's probably a bad ifCondition definition than a problem with the types (or maybe both). I tried to play around with getting ifCondition to merge the predicate props with the inner component props, but I think it might be stretching how far TS is willing to let you stretch things out, currying wise.

/>
),
'instanceId'
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several things wrong about the withInstanceId types:

  • extends ComponentProps< any > is redudant because ComponentProps never gets a reasonable component type to infer props from. The result is either {} or any.
  • the withInstanceId HOC accepts also components that don't accept instanceId as a prop, which is an undetected type error.
  • the withInstanceId HOC returns a component that accepts the instanceId prop, which is an error, too. The outer component doesn't accept that prop, because it's supposed to be injected.

@jsnajdr
Copy link
Member

jsnajdr commented May 16, 2022

Today I tried to do this:

type InstanceIdProps = { instanceId: string | number };

type Combine< P, I > = Omit< P, keyof I > & I;

function addInstanceId<
  C extends ComponentType< Combine< ComponentProps< C >, InstanceIdProps > >
>( WrappedComponent: C ) {
  return ( props: Omit< ComponentProps< C >, keyof InstanceIdProps > ) => {
    const instanceId = useInstanceId( WrappedComponent );
    const wrappedProps = { ...props, instanceId } as ComponentProps< C >;
    return <WrappedComponent { ...wrappedProps } />;
  };
}

const withInstanceId = createHigherOrderComponent( addInstanceId, 'instanceId' );

The types of addInstanceId check that the wrapped component (of type C) accepts an instanceId prop and rejects components that don't.

But there are two errors reported. First, TypeScript doesn't accept wrappedProps as valid props for WrappedComponent. I don't know why, the errors involve only only types from @types/react, but also internal TypeScript types for JSX elements: IntrinsicAttributes and LibraryManagedAttributes.

Second, addInstanceId's type doesn't match with what createHigherOrderComponent expects. I think the reason is that type parameters don't match. addInstanceId has one type parameter, a component type, while createHigherOrderComponent expects mapComponent with two type parameters, Inner and Outer, which are not component types, but props types. And when TypeScript tries to interpret the component type as a props type, it fails, because the types really don't match.

@jsnajdr
Copy link
Member

jsnajdr commented May 16, 2022

I found some tricks in @types/react-redux that could help solve the above issues 👍 Going to try them out.

@dmsnell
Copy link
Member Author

dmsnell commented May 17, 2022

calling ifBar with a component that does not accept bar should be an error,

while reviewing I did consider this and thought it shouldn't be an error, particularly because the if condition can wrap a component and look at any kind of prop to determine visibility; it seems plausible that we might want to govern that with a prop the underlying component doesn't support.


this reminds me that there are two levels interacting here that probably have been keeping this confusing: every HOC has a different type from every other HOC and that's separate from the type of createHigherOrderComponent.

can we think of a way to split this so that we can free the individual HOCs' types from createHigherOrderComponent in order to let us address each separately? this is making me think again of relaxing the types or making createHigherOrderComponent untyped for now.

@jsnajdr will still take me some time to process what you wrote but thank you for continuing to work on this with us.

@jsnajdr
Copy link
Member

jsnajdr commented May 17, 2022

the if condition can wrap a component and look at any kind of prop to determine visibility; it seems plausible that we might want to govern that with a prop the underlying component doesn't support

The HOC, however, passes all props down to the wrapped component, there is no way to opt out of that.

@sarayourfriend
Copy link
Contributor

the if condition can wrap a component and look at any kind of prop to determine visibility; it seems plausible that we might want to govern that with a prop the underlying component doesn't support

The HOC, however, passes all props down to the wrapped component, there is no way to opt out of that.

True, but not from a type perspective. If you read on in my comment I speculate that the issue isn't actually that the ifBar(NoBarPropComponent) call isn't an error, it just produces the wrong type, currently ComponentType<any>, which is useless. It should produce something like ComponentType<Paramters<typeof ifBar>[0] & ComponentProps<NoBarPropComponent>>. I tried to make this work but I couldn't figure it out and currently don't have that much time to spend on this.

I'd say in this sense that the original implementation was incorrect, it should not be an error, but it also shouldn't produce a uselessly wide type.

@dmsnell
Copy link
Member Author

dmsnell commented May 20, 2022

The HOC, however, passes all props down to the wrapped component, there is no way to opt out of that.

What I think the accurate type is follows something like this:

  • The inner component need not demand those props
  • If it does indicate props of the names given by the HOC then the types of those props should correspond.

This sounds like what you found with the Redux typings. <C extends ComponentType<Matching<Injected,ComponentProps<C>>(component: C) => ComponentType<Omit<ComponentProps<C>, keyof Injected>;

I'd say in this sense that the original implementation was incorrect, it should not be an error, but it also shouldn't produce a uselessly wide type.

This is where I'm kind of giving up until we have everything truly worked out. I don't know how to avoid the false error without widening, but one thing we can do is allow the actual HOCs (the mapComponent function as cHOC calls it) to maintain their own types, in which case they are free to make assertions that cHOC can't. In other words, by stepping this function back away from the center stage it lets us address the type challenges on a case-by-case basis instead of demanding that we sole the universal case that covers every possible incantation.

@jsnajdr
Copy link
Member

jsnajdr commented May 23, 2022

What I think the accurate type is follows something like this:

  • The inner component need not demand those props
  • If it does indicate props of the names given by the HOC then the types of those props should correspond.

I believe that any HOC written with the InjectedProps helpers satisfies this, and it's because of how TypeScript itself works.

When there is a function that accepts foo:

const Foo = ( props: { foo: string } ) => null;

I can call it with a props object that has additional fields:

const theProps = { foo: 'x', instanceId: 1 };
Foo( theProps ); // this is fine

Because the type of theProps is a subtype of the props parameter type.

The same is acceptable when written as JSX:

return <Foo { ...theProps } />

Nobody demands that the Foo function declares and accepts the instanceId prop.

However, there's one situation where TypeScript actually demands this and which confused me a lot until I realized what's going on:

Foo( { foo: 'x', instanceId: 1 } );

Here TypeScript will report an error:

Object literal may only specify known properties, and 'instanceId' does not exist in type '{ foo: string }'

Although the parameter has a valid type (subtype of the param type), TypeScript won't allow it because it's written as object literal. Once I introduce a helper theProps variable, as above, it starts to be OK again.

The same error will happen with JSX written as:

return <Foo foo="x" instanceId={ 1 } />

Here the error will be different:

Type '{ foo: string; instanceId: number; }' is not assignable to type 'IntrinsicAttributes & { foo: string; }'.
  Property 'instanceId' does not exist on type 'IntrinsicAttributes & { foo: string; }'.

But it's exactly the same situation as the object literal, it's just the error messages that are different for the JSX and non-JSX code path.

We need to keep this in mind when writing HOCs, because

const wrappedProps = { ...props, injectedProp };
return <Wrapped { ...wrappedProps } />;

and

return <Wrapped { ...props } injectedProp={ injectedProp } />

both compile to exactly the same code, but one is TS-ok and the other is TS-error.

@jsnajdr
Copy link
Member

jsnajdr commented May 23, 2022

one thing we can do is allow the actual HOCs (the mapComponent function as cHOC calls it) to maintain their own types, in which case they are free to make assertions that cHOC can't. In other words, by stepping this function back away from the center stage it lets us address the type challenges on a case-by-case basis

The cHOC types in #41138 should solve this. The cHOC type, with two type parameters which both extend Component<any>, places only the most minimal constraints on mapComponent. And all the magic and props transformations are concentrated in the type of the mapComponent itself.

@dmsnell
Copy link
Member Author

dmsnell commented May 24, 2022

However, there's one situation where TypeScript actually demands this and which confused me a lot until I realized what's going on:

Oh boy. Do you understand why it does this? I guess on one hand it makes sense to say "hey, you're typing these things by hand here, but you know that property will be ignored right? maybe it's a typo?" but on the other hand it seems odd when TS is happy if you split that inline expression into a variable holding a literal value and a call referencing the value.

In reflecting on what you wrote I do believe I've seen this a number of times without realizing that the important distinction is whether or not it's a literal value in an inferred context like that.

@jsnajdr
Copy link
Member

jsnajdr commented May 30, 2022

Oh boy. Do you understand why it does this?

It sounds like a reasonable thing to do: you can always change the literal to remove the excess properties, and as the literal is used only once at that particular place, it doesn't affect anything else. So TypeScript forces you to remove them.

If you split a variable, TypeScript would need to check that the variable is really used only once, and that's additional flow analysis and a much more complex check to do.

What I don't like about this and what I believe is fixable, is the error message in the JSX case. Assigning object literals, TS tells you exactly what it doesn't like:

Type '{ foo: string; bar: string; }' is not assignable to type 'Foo'.
  Object literal may only specify known properties, and 'bar' does not exist in type 'Foo'.

But in the JSX case the same error is more obfuscated:

Type '{ foo: string; bar: string; }' is not assignable to type 'FooProps'.
  Property 'bar' does not exist on type 'FooProps'.

@dmsnell
Copy link
Member Author

dmsnell commented Jun 8, 2022

closing in favor of #41138

@dmsnell dmsnell closed this Jun 8, 2022
@dmsnell dmsnell deleted the types/fix-create-higher-order-component branch June 8, 2022 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Compose /packages/compose [Type] Code Quality Issues or PRs that relate to code quality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants