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: Rework types of createHigherOrderComponent for closer match to API #37795

Merged
merged 7 commits into from
Jan 24, 2022

Conversation

dmsnell
Copy link
Member

@dmsnell dmsnell commented Jan 7, 2022

Status

Ready for final review ✓

There are a couple things I'm unhappy with about this refactor. I think it's more accurate
than the existing types, which I find confusing and think might be wrong. Still, it doesn't
handle components with ref parameters well (see with-global-events) and I would
prefer that we have full inference with fewer necessary type annotations.

Update: For now I want to ignore the ref issues and with-global-events since that
component has a number of more difficult type issues and it's marked as deprecated.
Let's not hold up this work to spend time describing a function we don't want people using.

Type annotations we don't want Everything is passing the tests right now though so I don't want to totally hold off on this, but I'd like to let it stew for a bit and see if anyone has suggestions on how to close the gap. For reference, here is how I think `withInstanceId` should be able to be written (pay attention to the lack of type annotations - we shouldn't be losing any auto-complete or type safety).
const withInstanceId = createHigherOrderComponent< {
	instanceId: string | number;
} >( WrappedComponent ) => {
	return ( props ) => {
		const instanceId = useInstanceId( WrappedComponent );
			return (
				<WrappedComponent
					{ ...props }
					instanceId={ instanceId }
				/>
			);
		};
	},
	'withInstanceId'
);

Description

The existing types in createHigherOrderComponent<TOuter, TInner> are somewhat
surprising because they require defining what possible prop types are allowed
for components wrapped by the higher order component it produces. Additionally
it doesn't concern itself with preventing the wrapped component from accepting
or demanding props it itself provides.

In this patch we're reworking the types to only require a single type parameter,
which is the type of props that the higher order component injects into the
wrapped component. The prop types for the wrapped component are inferred only
when wrapping a component, and the injected props are removed from the external
interface to avoid confusion.

How has this been tested?

This is a type-only change that shouldn't impact the generated bundles.

Types of changes

Updating type signature for createHigherOrderComponent

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • I've tested my changes with keyboard and screen readers.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR (please manually search all *.native.js files for terms that need renaming or removal).
  • I've updated related schemas if appropriate.

cc: @sirbrillig

@dmsnell dmsnell requested a review from sarayourfriend January 7, 2022 21:38
@github-actions
Copy link

github-actions bot commented Jan 7, 2022

Size Change: 0 B

Total Size: 1.13 MB

ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 960 B
build/admin-manifest/index.min.js 1.1 kB
build/annotations/index.min.js 2.75 kB
build/api-fetch/index.min.js 2.21 kB
build/autop/index.min.js 2.12 kB
build/blob/index.min.js 459 B
build/block-directory/index.min.js 6.28 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 141 kB
build/block-editor/style-rtl.css 14.6 kB
build/block-editor/style.css 14.6 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/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 470 B
build/block-library/blocks/button/editor.css 470 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 90 B
build/block-library/blocks/code/style.css 90 B
build/block-library/blocks/code/theme-rtl.css 131 B
build/block-library/blocks/code/theme.css 131 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-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/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.22 kB
build/block-library/blocks/cover/style.css 1.22 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 322 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 965 B
build/block-library/blocks/gallery/editor.css 967 B
build/block-library/blocks/gallery/style-rtl.css 1.6 kB
build/block-library/blocks/gallery/style.css 1.6 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 159 B
build/block-library/blocks/group/editor.css 159 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 114 B
build/block-library/blocks/heading/style.css 114 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 810 B
build/block-library/blocks/image/editor.css 809 B
build/block-library/blocks/image/style-rtl.css 507 B
build/block-library/blocks/image/style.css 511 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 447 B
build/block-library/blocks/latest-posts/style.css 446 B
build/block-library/blocks/list/style-rtl.css 94 B
build/block-library/blocks/list/style.css 94 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 649 B
build/block-library/blocks/navigation-link/editor.css 650 B
build/block-library/blocks/navigation-link/style-rtl.css 94 B
build/block-library/blocks/navigation-link/style.css 94 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 343 B
build/block-library/blocks/navigation/editor-rtl.css 1.99 kB
build/block-library/blocks/navigation/editor.css 2 kB
build/block-library/blocks/navigation/style-rtl.css 1.85 kB
build/block-library/blocks/navigation/style.css 1.84 kB
build/block-library/blocks/navigation/view.min.js 2.81 kB
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 401 B
build/block-library/blocks/page-list/editor.css 402 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 273 B
build/block-library/blocks/paragraph/style.css 273 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/style-rtl.css 446 B
build/block-library/blocks/post-comments-form/style.css 446 B
build/block-library/blocks/post-comments/style-rtl.css 521 B
build/block-library/blocks/post-comments/style.css 521 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 389 B
build/block-library/blocks/pullquote/style.css 388 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 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 187 B
build/block-library/blocks/quote/style.css 187 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 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 99 B
build/block-library/blocks/separator/editor.css 99 B
build/block-library/blocks/separator/style-rtl.css 245 B
build/block-library/blocks/separator/style.css 245 B
build/block-library/blocks/separator/theme-rtl.css 172 B
build/block-library/blocks/separator/theme.css 172 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 744 B
build/block-library/blocks/site-logo/editor.css 744 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.32 kB
build/block-library/blocks/social-links/style.css 1.32 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 471 B
build/block-library/blocks/table/editor.css 472 B
build/block-library/blocks/table/style-rtl.css 481 B
build/block-library/blocks/table/style.css 481 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 214 B
build/block-library/blocks/tag-cloud/style.css 215 B
build/block-library/blocks/template-part/editor-rtl.css 560 B
build/block-library/blocks/template-part/editor.css 559 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 908 B
build/block-library/common.css 905 B
build/block-library/editor-rtl.css 10.1 kB
build/block-library/editor.css 10.1 kB
build/block-library/index.min.js 166 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 10.8 kB
build/block-library/style.css 10.8 kB
build/block-library/theme-rtl.css 672 B
build/block-library/theme.css 676 B
build/block-serialization-default-parser/index.min.js 1.09 kB
build/block-serialization-spec-parser/index.min.js 2.79 kB
build/blocks/index.min.js 46.4 kB
build/components/index.min.js 215 kB
build/components/style-rtl.css 15.5 kB
build/components/style.css 15.5 kB
build/compose/index.min.js 11.2 kB
build/core-data/index.min.js 13.3 kB
build/customize-widgets/index.min.js 11.4 kB
build/customize-widgets/style-rtl.css 1.5 kB
build/customize-widgets/style.css 1.49 kB
build/data-controls/index.min.js 631 B
build/data/index.min.js 7.49 kB
build/date/index.min.js 31.9 kB
build/deprecated/index.min.js 485 B
build/dom-ready/index.min.js 304 B
build/dom/index.min.js 4.5 kB
build/edit-navigation/index.min.js 16 kB
build/edit-navigation/style-rtl.css 3.76 kB
build/edit-navigation/style.css 3.76 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 29.6 kB
build/edit-post/style-rtl.css 7.15 kB
build/edit-post/style.css 7.14 kB
build/edit-site/index.min.js 37.7 kB
build/edit-site/style-rtl.css 6.85 kB
build/edit-site/style.css 6.84 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.17 kB
build/edit-widgets/style.css 4.17 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 3.29 kB
build/escape-html/index.min.js 517 B
build/format-library/index.min.js 6.58 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.63 kB
build/html-entities/index.min.js 424 B
build/i18n/index.min.js 3.75 kB
build/is-shallow-equal/index.min.js 501 B
build/keyboard-shortcuts/index.min.js 1.8 kB
build/keycodes/index.min.js 1.39 kB
build/list-reusable-blocks/index.min.js 1.72 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.92 kB
build/notices/index.min.js 925 B
build/nux/index.min.js 2.08 kB
build/nux/style-rtl.css 747 B
build/nux/style.css 743 B
build/plugins/index.min.js 1.84 kB
build/primitives/index.min.js 924 B
build/priority-queue/index.min.js 582 B
build/react-i18n/index.min.js 671 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.65 kB
build/reusable-blocks/index.min.js 2.22 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11 kB
build/server-side-render/index.min.js 1.58 kB
build/shortcode/index.min.js 1.49 kB
build/token-list/index.min.js 639 B
build/url/index.min.js 1.9 kB
build/viewport/index.min.js 1.05 kB
build/warning/index.min.js 248 B
build/widgets/index.min.js 7.15 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

Comment on lines 4 to 5
// eslint-disable-next-line no-restricted-imports
import type { ComponentType } from 'react';
Copy link
Member

Choose a reason for hiding this comment

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

Instead of telling ESLint to shut up, how about we export that type from our package instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really telling it to "shut up" 😆

There are almost ninety other instances of disabling this error for a type import. I'm happy to try and get back to adjust @wordpress/element but it's still a JavaScript file without exported types. I'd prefer to not hold up this PR while waiting to change @wordpress/element and update all of those other existing cases.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

It's disabling this rule:

gutenberg/.eslintrc.js

Lines 84 to 88 in cc569b2

{
name: 'react',
message:
'Please use React API through `@wordpress/element` instead.',
},

It was discussed several times before. Ideally, we would re-import APIs we use from @wordpress/element for consistency with how React is used. However, it never seemed to be a simple task with TypeScript. People started disabling this rule when importing rules. We should instead seek ways to update this rule so it doesn't error for types.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

PR is ready: #37862.

Copy link
Member Author

Choose a reason for hiding this comment

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

rebased against new trunk, which contains #37862
thanks again @gziolo !

@@ -72,8 +71,9 @@ const withSafeTimeout: PropInjectingHigherOrderComponent< TimeoutProps > = creat
...this.props,
setTimeout: this.setTimeout,
clearTimeout: this.clearTimeout,
};
return <OriginalComponent { ...( props as TProps ) } />;
} as TProps;
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this type assertion?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not totally confident on my understand here, but I believe that it's because of the Omit. What I'm trying to convey is that the wrapped component doesn't want the timer functions to be passed in from the outside, but it still provides them to the wrapped component. I think there could be an issue here with React saying that props could contain any number of properties and TypeScript wants to complain that we might get ones we don't want or expect.

Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this type assertion?

It's described in this TS issue: microsoft/TypeScript#35858 (comment)

Quote:

TS isn't capable of the higher-order reasoning needed to understand that Exclude<T, k> & { [k]: T[k] } is equivalent to T.

If you observe how the type of props is Omit<TProps, ...> and how the value of local props value is constructed with { ...this.props, ...}, it's exactly the same situation.

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

This is awesome. I did a little testing on this branch and everything I tried seemed to work great. @jsnajdr and I spent a considerable amount of time trying to find a solution for this, I'm glad there was something simpler and much more straightforward to use possible! Really nice work. Will be happy to review again once it's converted from a draft to a full PR.

Copy link
Contributor

@sarayourfriend sarayourfriend left a comment

Choose a reason for hiding this comment

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

Looks great to me! I would love a second opinion from @jsnajdr who tends to see things I miss! But otherwise thanks so much for finding this significant improvement to the usability of this utility.

…to API

The existing types in `createHigherOrderComponent<TOuter, TInner>` are somewhat
_surprising_ because they require defining what possible prop types are allowed
for components wrapped by the higher order component it produces. Additionally
it doesn't concern itself with preventing the wrapped component from accepting
or demanding props it itself provides.

In this patch we're reworking the types to only require a single type parameter,
which is the type of props that the higher order component injects into the
wrapped component. The prop types for the wrapped component are inferred only
when wrapping a component, and the injected props are removed from the external
interface to avoid confusion.
@dmsnell dmsnell force-pushed the types/createHigherOrderComponent branch from 1af7598 to 7971f9f Compare January 24, 2022 16:19
@dmsnell
Copy link
Member Author

dmsnell commented Jan 24, 2022

Rebased against the fix to docgen in #37929.

@dmsnell
Copy link
Member Author

dmsnell commented Jan 24, 2022

cc: @sirbrillig keep an eye out for this in the next release ✅

@dmsnell
Copy link
Member Author

dmsnell commented Jan 24, 2022

Merging for lack of further review. Should anyone notice any problems with this please don't hesitate to ping me or ask for further refinement.

@dmsnell dmsnell merged commit 80eb2d6 into trunk Jan 24, 2022
@dmsnell dmsnell deleted the types/createHigherOrderComponent branch January 24, 2022 22:19
@github-actions github-actions bot added this to the Gutenberg 12.5 milestone Jan 24, 2022
@noahtallen
Copy link
Member

I'm just now getting around to updating this package in a 3rd party repo. We have this use case:

export const withLocale = createHigherOrderComponent< { locale: string } >( ( InnerComponent ) => {
	return ( props ) => {
		const locale = useLocale();
		return <InnerComponent { ...props } locale={ locale } />;
	};
}, 'withLocale' );

Which fails compilation with:

packages/i18n-utils/src/locale-context.tsx(99,11): error TS2322: Type 'Omit<InnerProps, "locale"> & { locale: string; children?: ReactNode; }' is not assignable to type 'IntrinsicAttributes & InnerProps & { children?: ReactNode; }'.
  Type 'Omit<InnerProps, "locale"> & { locale: string; children?: ReactNode; }' is not assignable to type 'InnerProps'.
    'Omit<InnerProps, "locale"> & { locale: string; children?: ReactNode; }' is assignable to the constraint of type 'InnerProps', but 'InnerProps' could be instantiated with a different subtype of constraint '{ locale: string; }'.

To me, it seems like this super basic use case should be supported without adding, e.g. @ts-ignore. Maybe I'm missing something? It seems like ts-ignore was used in this PR to fix the instance ID hook.

@jsnajdr
Copy link
Member

jsnajdr commented May 2, 2022

@noahtallen I was able to make withLocale work by adding some explicit types:

type LocaleProps = { locale: string };
export const withLocale = createHigherOrderComponent< LocaleProps >(
  < TProps extends LocaleProps >( InnerComponent: React.ComponentType< TProps > ) => {
    return ( props: Omit< TProps, keyof LocaleProps > ) => {
      const locale = useLocale();
      const innerProps = { locale, ...props } as TProps;
      return <InnerComponent { ...innerProps } />;
    };
  },
  'withLocale'
);

Note that Gutenberg's withSafeTimeout HOC uses the same technique.

The culprit is the same as described in #37795 (comment). TypeScript infers the InnerProps type from the type of InnerComponent, but then is unable to figure out that Omit< InnerProps, 'locale' > & { locale: string } is equivalent to InnerProps. We humans understand that removing the locale field and then adding it back again leads to the original InnerProps type, but TypeScript doesn't. So there needs to be an explicit as InnerProps cast. But to get access to InnerProps, I need to declare the entire type of the mapComponent function explicitly.

Typing HOCs in TypeScript is incredibly painful, the two concepts are not very compatible. Everything's much simpler when everything is just hooks and functional components.

@jsnajdr
Copy link
Member

jsnajdr commented May 2, 2022

...and here's an easier way to fix withLocale, utilizing the ComponentProps helper that extracts the InnerProps type from InnerComponent:

const innerProps = { locale, ...props } as React.ComponentProps<typeof InnerComponent>;
return <InnerComponent { ...innerProps } />;

@dmsnell
Copy link
Member Author

dmsnell commented May 4, 2022

Thanks @jsnajdr for the sleuthing. I am hoping too that the newer in/out covariant/contravariant syntax will let us clean this up nicely. Do you propose we update the types internally in the meantime to somehow fix this?

Typing HOCs in TypeScript is incredibly painful, the two concepts are not very compatible.

No need to clarify, but I'm curious what mismatch you are talking about. When working on this I did find some situations frustrating in that I thought I should be able to rely on the type inference but couldn't.

If you have a better idea at the fundamental issues maybe that would help us find a better solution?

@jsnajdr
Copy link
Member

jsnajdr commented May 5, 2022

No need to clarify, but I'm curious what mismatch you are talking about. When working on this I did find some situations frustrating in that I thought I should be able to rely on the type inference but couldn't.

It's just that when typing a HOC, things are never simple. You do something wrong, and suddenly you get a very abstract warning like:

Type 'X' is not assignable to type 'Y'. 'X' is assignable to the constraint of type 'Y',
but 'Y' could be instantiated with a different subtype of constraint 'Z'.

One needs to do some considerable mathematical thinking to unpack this.

Because everything is hard, the types are rarely pleasant to work with, like having inference where one would expect it, or are just plain wrong. Consider this usage of ifCondition:

type PropsOne = { name: string };
type PropsTwo = { count: number };

const Foo = ( { name }: PropsOne ) => <div>{ name }</div>;
const predicate = ( { count }: PropsTwo ) => count > 0;

const IfFoo = ifCondition( predicate )( Foo );

There are types defined, typechecking passes successfully, but there is an obvious type mismatch: the predicate argument must be the of same type as the props of Foo, but they are completely different and TypeScript didn't catch it.

When I tried to fix ifCondition to ensure that the types of predicate and Foo are compatible, and to make ifCondition( predicate ) accept only components whose props match the predicate (it can't accept just any component, like other HOCs!), I got this:

Argument of type '<InnerProps extends {}>(WrappedComponent: ComponentType<TProps>) => (props: TProps) =>
Element | null' is not assignable to parameter of type 'HigherOrderComponent<{}>'. Types of parameters
'WrappedComponent' and 'Inner' are incompatible. Type 'ComponentType<InnerProps>' is not assignable to type
'ComponentType<TProps>'. Type 'ComponentClass<InnerProps, any>' is not assignable to type 'ComponentType<TProps>'.
Type 'ComponentClass<InnerProps, any>' is not assignable to type 'ComponentClass<TProps, any>'. Types of property
'propTypes' are incompatible. Type 'WeakValidationMap<InnerProps> | undefined' is not assignable to type
'WeakValidationMap<TProps> | undefined'. Type 'WeakValidationMap<InnerProps>' is not assignable to type
'WeakValidationMap<TProps>'. Type 'keyof TProps' is not assignable to type 'keyof InnerProps'. Type 'string | number |
symbol' is not assignable to type 'keyof InnerProps'. Type 'string' is not assignable to type 'keyof InnerProps'. Type 'string'
is not assignable to type 'never'. Type 'keyof TProps' is not assignable to type 'never'. Type 'string | number | symbol' is not
assignable to type 'never'. Type 'string' is not assignable to type 'never'.

How to proceed from here? It's no longer fun at all.

Also, I don't know what was the motivation for the changes you did in this PR, you merely wrote that "existing types in createHigherOrderComponent<TOuter, TInner> are somewhat
surprising". But it seems to me that declaring the TOuter and TInner types was more correct than declaring the "difference".

A HOC is a function that transforms one component type to another, and their props don't need to be related at all. The case where new props are injected and old ones are passed is merely a special case, although the most frequent one. It's like when the type of Array<A>.map<B> is ( value: A ) => B, with no relation between A and B.

@dmsnell
Copy link
Member Author

dmsnell commented May 5, 2022

Thanks for clarifying. I'll work on this and see if we can't make those errors clearer. I don't know if this feels like an inherent mismatch though that would be better with hooks, unless the claim is that with hooks we just don't have to worry about wrapped props. It looks like you have pointed out a flaw in my assumptions in #37795

Noted in that PR I wasn't fully confident in the solution, but I did let it sit open for a few weeks to try and find review on it at the time. The motivation was some confusion around how to properly type the HOC. IIRC I was primarily considering the prop-injecting HOC pattern and wanted to warn people if they were trying to supply a prop that would ultimately be ignored. In Gutenberg I see counter-examples such as the color-providing HOC.

So again I think you're right in pointing out that the types were more correct before. There may still be a way to revert to that while removing the additional type wrappers like PropInjectingHigherOrderComponent that might have muddied the water.

@jsnajdr
Copy link
Member

jsnajdr commented May 9, 2022

I was primarily considering the prop-injecting HOC pattern and wanted to warn people if they were trying to supply a prop that would ultimately be ignored. In Gutenberg I see counter-examples such as the color-providing HOC.

Is there a specific example of a HOC that had troubles with the old types and got better with the new types? I'd like to understand the issues better.

At this moment I feel that we want to revert this PR to the old state, but not completely? There are some use cases that this PR actually improved, and we want to do some kind of synthesis?

@dmsnell
Copy link
Member Author

dmsnell commented May 10, 2022

Looking back on some issues with Calypso code what I believe was happening is that the type of InnerProps was enforced for all invocations of the produced higher-order-component when in fact the produced higher-order-component shouldn't demand the same input props to the inner component.

https://github.com/WordPress/gutenberg/pull/37795/files#diff-05e2e8846bd708cfa15b312de800aa48fdab481b284063a3e7de6d19fd245717L39-L44

Or put more minimally, we had the kind of problem of using this…

interface ToBinary<T> {
    (input: T): Binary;
}

…when we want this instead…

interface ToBinary {
    <T>(input: T): Binary;
}

Screen Shot 2022-05-10 at 1 50 07 PM

You can see in this patch where I was trying to do that, but added the unnecessary and inaccurate constraint that InnerProps extends HOCProps

dmsnell added a commit that referenced this pull request May 11, 2022
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 added a commit that referenced this pull request May 12, 2022
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.
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.

7 participants