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

[data] Fill out type definition for data registry #39081

Open
wants to merge 10 commits into
base: trunk
Choose a base branch
from

Conversation

dmsnell
Copy link
Member

@dmsnell dmsnell commented Feb 24, 2022

Description

The type for the data registry created with createRegistry() has lived a
split and minimal life: split across a basic type declaration in types.ts
and the associated JSDoc @typedef in registry.js; and minimal because
neither definition comprehensively describes the structure.

In this patch we're unifying the type into the TypeScript declaration and
also filling it out to comprehensively describe the registry. The complicated
type in this patch is the result of several different attempts to provide the
following goals:

  • Ultimately provide auto-complete for the core data stores in select() and
    dispatch() calls.
  • Require minimal effort to bring existing core data stores into the default
    core registry: no new type annotations in packages outside of @wordpress/data
    and no duplicating type information about other packages inside of @wordpress/data.
  • Provide an interface that third-party developers can extend with their own
    stores and not be required to give up any of the core store information.

In this patch the data registry is empty because I want to focus on the type itself.
I believe that adding additional core stores will require at a minimum, renaming some files to end in .ts and activating TypeScript for that package if it isn't already.

Testing Instructions

As a type-only change there should be no impact on the built files.
For testing and review please audit the code and types.

Screenshots

While not enabled in this PR itself, by using typeof storeConfig in other packages we can get the list of actions and selectors (with the state parameter curried away) for that store. The arguments and return types won't be typed because they only exist as JS, but we still get their names and the names of their argument and as we add typing to them we'll benefit through the registry.

Screen.Recording.2022-02-23.at.9.20.48.PM.mov
Screen.Recording.2022-02-24.at.12.29.51.PM.mov

Types of changes

Updating an existing type definition and merging a JSDoc @typedef into it.

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.

@github-actions
Copy link

github-actions bot commented Feb 25, 2022

Size Change: 0 B

Total Size: 1.21 MB

ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/admin-manifest/index.min.js 1.24 kB
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.49 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 146 kB
build/block-editor/style-rtl.css 15.4 kB
build/block-editor/style.css 15.4 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 59 B
build/block-library/blocks/avatar/editor.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-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-query-loop/editor-rtl.css 95 B
build/block-library/blocks/comments-query-loop/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.56 kB
build/block-library/blocks/cover/style.css 1.56 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 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 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 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 708 B
build/block-library/blocks/navigation-link/editor.css 706 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 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.89 kB
build/block-library/blocks/navigation/style.css 1.88 kB
build/block-library/blocks/navigation/view.min.js 2.85 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 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 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 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 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 201 B
build/block-library/blocks/quote/style.css 201 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 140 B
build/block-library/blocks/separator/editor.css 140 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 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 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 235 B
build/block-library/blocks/template-part/editor.css 235 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 934 B
build/block-library/common.css 932 B
build/block-library/editor-rtl.css 10 kB
build/block-library/editor.css 10 kB
build/block-library/index.min.js 171 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 11.2 kB
build/block-library/style.css 11.2 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 46.8 kB
build/components/index.min.js 218 kB
build/components/style-rtl.css 14.9 kB
build/components/style.css 14.9 kB
build/compose/index.min.js 11.2 kB
build/core-data/index.min.js 14.3 kB
build/customize-widgets/index.min.js 11.2 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 8.19 kB
build/date/index.min.js 31.9 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.53 kB
build/edit-navigation/index.min.js 16.1 kB
build/edit-navigation/style-rtl.css 4.04 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/index.min.js 29.8 kB
build/edit-post/style-rtl.css 7.07 kB
build/edit-post/style.css 7.07 kB
build/edit-site/index.min.js 45 kB
build/edit-site/style-rtl.css 7.58 kB
build/edit-site/style.css 7.56 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.4 kB
build/edit-widgets/style.css 4.39 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.29 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.12 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.2 kB
build/primitives/index.min.js 949 B
build/priority-queue/index.min.js 611 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.1 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

* This type is a convenience wrapper for turning that reference into
* an actual store regardless of what was passed.
*
* Warning! Will fail if given a name not already registered. In such a
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

@adamziel
Copy link
Contributor

adamziel commented Mar 3, 2022

This is great @dmsnell, thank you for working on it!

While not enabled in this PR itself, by using typeof storeConfig in other packages we can get the list of actions and selectors (with the state parameter curried away) for that store. The arguments and return types won't be typed because they only exist as JS, but we still get their names and the names of their argument and as we add typing to them we'll benefit through the registry.

For posterity, here's a snippet I used for testing:

const actions = {
	dispatchMyAction: ( nb: number ) => {
		return 'test';
	},
};
const selectors = {
	testFn: () => {},
};
export interface StoreConfig
	extends ReduxStoreConfig< any, typeof actions, typeof selectors > {}

export interface Stores extends DataStores {
	test: StoreConfig;
}
const dr = {} as DataRegistry;
const thisShouldBeAString = dr.dispatch( 'test' ).dispatchMyAction( 12 );

I noticed this doesn't yet support thunks , which means TS thinks the following action returns a function and not a number:

const actions = {
	iAmAThunk: ( nb: number ) => ( { select } ) => {
		return 'test';
	},
};
const result = dr.dispatch( 'test' ).iAmAThunk( 12 );
// Result is of type ( { select: any } ) => string

It would be great to make that call return the string directly. If it would be easy to type the thunk argument, that's even better.

How to identify a thunk? It's just any action or resolver at all that returns a function:

export default function createThunkMiddleware( args ) {
return () => ( next ) => ( action ) => {
if ( typeof action === 'function' ) {
return action( args );
}
return next( action );
};
}

The following snippet has worked for me:

type UnwrapThunks< Actions extends Record< string, Function > > = {
	[ k in keyof Actions ]: Actions[ k ] extends (
		...args: infer Args
	) => ( ...nestedArgs: any ) => infer RetVal
		? ( ...args: Args ) => RetVal
		: Actions[ k ];
};

type test = UnwrapThunks< {
	test: ( a: number ) => ( thunkArgs: any ) => string;
} >;
// test is ( a: number ) => string

We could even introduce a ThunksArg type that these actions could use. Then we could require the nested function to have zero or one arguments of that type. One challenge with having such type, though, is the following use-case:

function toggleFeatureValueAction ( { select, dispatch } ) {
	const currentValue = select.isFeatureActive( scope, featureName );
	dispatch.setFeatureValue( scope, featureName, ! currentValue );
};

dispatch would need to be familiar with all the selectors in the current store. Perhaps it wouldn't be that hard with the setup in this PR? I imagine it could be parametrized by StoreConfig like ThunkArg<StoreConfig>.

cc @jsnajdr

@adamziel
Copy link
Contributor

adamziel commented Mar 4, 2022

It would also be great to reuse selectors type signatures with resolvers, e.g.:

// selector
function getAuthors( state : State, query : Record<string, string> ) : Author[] | null { /* ... */ }

// resolver
export const getAuthors : ResolverOf< typeof selectors.getAuthors > = ( query ) => async ( { dispatch } ) => { 
    // ...
   return undefined; // Resolvers typically don't return anything
}
// query is Record<string, string> by inference
// getAuthors is of type resembling ( query : Record<string, string> ) => ThunkOrValue<Promise<undefined>>

@dmsnell dmsnell force-pushed the types/expand-data-registry branch from 4786f9d to 5a908db Compare March 4, 2022 22:18
Comment on lines +157 to +184
): NonNullable<
{
[ Name in keyof Selectors ]: CurriedState< Selectors[ Name ] >;
}
>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Something about the type inference here isn't exact:

CleanShot 2022-03-09 at 11 16 54

The error doesn't show up when I use the getEntityRecord selector directly without going through select.

I've had a similar problem in #39025 and ended up with one of these super long type declarations that test for every number of arguments separately:

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;
type ReplaceReturnType<T, TNewReturn> = T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => infer R ? (
    IsValidArg<J> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => TNewReturn :
    IsValidArg<I> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => TNewReturn :
    IsValidArg<H> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => TNewReturn :
    IsValidArg<G> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => TNewReturn :
    IsValidArg<F> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F) => TNewReturn :
    IsValidArg<E> extends true ? (a: A, b: B, c: C, d: D, e: E) => TNewReturn :
    IsValidArg<D> extends true ? (a: A, b: B, c: C, d: D) => TNewReturn :
    IsValidArg<C> extends true ? (a: A, b: B, c: C) => TNewReturn :
    IsValidArg<B> extends true ? (a: A, b: B) => TNewReturn :
    IsValidArg<A> extends true ? (a: A) => TNewReturn :
    () => TNewReturn
) : never

Credit goes to the following SO answer.

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 think the error might be coming from the type for rememo there as I'm not seeing it. For my own testing I used this to get around having a real type for rememo, which reminds me, we should file a PR in DefinitelyTyped for it

const createSelector = rememoCreateSelect as <F>(selector: F, deps: any) => F;
Screen.Recording.2022-03-18.at.2.21.13.PM.mov

you can also see in that screencast my original intended method for adding core styles into the type system. typeof storeConfig, or in this case, since storeConfig is a function that produces it, ReturnType<typeof storeConfig> so we don't even need to know about ReduxStoreConfig (though it's obviously fine to manually add ReduxStoreConfig<…, …, …>)

Copy link
Contributor

Choose a reason for hiding this comment

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

@dmsnell Could it be that the selector types signature PR now stores these signatures in a separate file and doesn't associate them with the selector functions? Because this changed between my original comment and your reply. I'm asking because I noticed all the arguments are of the type any:

CleanShot 2022-03-20 at 20 52 26

And I hoped to see kind: Kind, name: Name, key: KeyOf< EntityRecordType< Kind, Name > > etc

Copy link
Member Author

Choose a reason for hiding this comment

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

Could it be that the #39025 now stores these signatures in a separate file and doesn't associate them with the selector functions?

I'm not sure what you mean here. This code is looking at the selectors directly and should report exactly what TypeScript knows about those selectors.

If you create ReduxStoreConfig<State, Actions, Selectors> manually then it might behave differently because we're telling the type system to trust that what we're saying is more accurate than what it infers. I don't think I understand your question exactly though, at least I am pretty sure I'm missing the question you are asking.

all the arguments are of the type any:

Since the selectors are written in JS we aren't getting types. This should match what info TypeScript has. As we add types or convert those modules to TypeScript the argument type information should appear automatically in this wrapped type. I'll try and get an example screencast of that.

Copy link
Contributor

@adamziel adamziel Mar 21, 2022

Choose a reason for hiding this comment

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

I'll try and get an example screencast of that.

What I meant was that once you wire the TypeScript type for the getEntityRecord selector instead of using the vanilla JS version, you will get the same signature error as on the original screenshot I posted above.

The reason it does not break like that at the moment is that the TS signature and the JS selector got decoupled in my PR after I initially reported the problem in this thread.

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 still not getting it, which may be because I just don't comprehend what's going on.

How does "a TS signature and JS selector [get] decoupled"?

Copy link
Contributor

@adamziel adamziel Mar 31, 2022

Choose a reason for hiding this comment

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

@dmsnell I meant the following timeline happened:

  1. I created TypeScript signatures for core-data selectors #39025 where I initially relied on typeDef. Importing getEntityRecord meant TypeScript knew that the first argument was kind: Kind, the second was name: Name and so on.
  2. You created this PR
  3. I tested how the two work together and realized that the @wordpress/data type definitions here do not preserve the correct signature for getEntityRecord – that's my comment above
  4. I removed the typeDef comments from TypeScript signatures for core-data selectors #39025 and moved the type signatures into a separate .ts file. After this moment, Typescript thinks that getEntityRecord takes arguments of type any because the full signature is not tied to the implementation in any way. In other words, importing getEntityRecord makes TS think that the first argument is kind: any, the second is name: any, and so on.
  5. You were unable to reproduce the problem from point 3 because the PR TypeScript signatures for core-data selectors #39025 was no longer the same as when I reported the problem.

I believe that a version of the core-data store where getEntityRecord has the following signature:

export type getEntityRecord = <
	R extends EntityRecordOf< K, N >,
	C extends Context = DefaultContextOf< R >,
	K extends Kind = KindOf< R >,
	N extends Name = NameOf< R >,
	/**
	 * The requested fields. If specified, the REST API will remove from the response
	 * any fields not on that list.
	 */
	Fields extends string[] | undefined = undefined
>(
	state: State,
	kind: K,
	name: N,
	key: KeyOf< R >,
	query?: EntityQuery< C, Fields >
) =>
	| ( Fields extends undefined
			? EntityRecordOf< K, N, C >
			: Partial< EntityRecordOf< K, N, C > > )
	| null
	| undefined;

would not work correctly with the data types currently proposed by this PR. How? In the way I initially described in the root comment of this discussion.

@dmsnell
Copy link
Member Author

dmsnell commented Mar 18, 2022

@adamziel that's a good catch. I wonder what's best here but I went ahead and added thunk-unwrapping only for those action creators which returns functions taking select and dispatch. I think this is going to fail at times and I hope it won't lead to incorrect type inference.

I almost went back and relaxed the constraint to any function at all, but instead I just made select and dispatch optional. If someone returns a function from an action creator that isn't a thunk it will blow up so I think we're safe on this one.

@dmsnell dmsnell force-pushed the types/expand-data-registry branch 2 times, most recently from 07c6580 to 32d9653 Compare March 18, 2022 20:58
@dmsnell
Copy link
Member Author

dmsnell commented Mar 18, 2022

I almost went back and relaxed the constraint to any function at all, but instead I just made select and dispatch optional. If someone returns a function from an action creator that isn't a thunk it will blow up so I think we're safe on this one.

Now I've gone back on going back and added a more generalized type. Noting here for a TODO that we also have "registry selectors" which accept select as a parameter instead of typical Redux thunks. with more typing work we should be able to handle these. For now I think we won't be able to add them until createRegistrySelector is typed and also that we account for that in our DataRegistry type. I'm afraid to tackle that with this big PR and I don't think it will block any work in the meantime (meaning we are unlikely to introduce false type errors before supporting registry selectors)

The `DataRegistry` type has been a hollow shell of its real shape.
In this patch we're adding basic types for all the methods and
properties it exposes to the app.

There is value to glean from expanding these types but as a starter
this update should prevent someone from getting a false type error
when attempting basic use of the registry in typed code.
@dmsnell
Copy link
Member Author

dmsnell commented Mar 24, 2022

All: thanks for the review. Once the tests pass I'm going to merge this and get it in so we can start building off of it. I believe that for the most part we will not be introducing false type errors with this PR, apart from the potential when dealing with registrySelectors.

Planned followup:

  • With this core type in place start adding stores from other packages into the global data store.
  • Start taking advantage of this type in the functions we care about, specifically moving towards useSelect and useDispatch
  • Pay attention to any issues that crop up and iterate on the type. Pay particular attention to type errors, dealing with the more complicated types, and any false type-errors.

@dmsnell
Copy link
Member Author

dmsnell commented Mar 24, 2022

I'm not able to merge this; is anyone willing to share in the journey and approve this PR? Or if you have objections, to mention them so I can address them? 🙇‍♂️

Copy link
Contributor

@ajlende ajlende left a comment

Choose a reason for hiding this comment

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

Things are mostly looking okay for me, but I was having issues getting types for resolvers to work as I expected.

The error I'm getting is that that the async function that resolvers return isn't expected in the type. For example, it's expecting (s: string) => Promise<void> instead of (s: string) => ({ dispatch }) => Promise<void> in the store config.

Example Code

const actions = {
	testDispatchAction: ( n: number ) => {
		return {
			type: 'TEST' as const,
			n,
		};
	},
	testDispatchAction2: ( s: string ) => {
		return {
			type: 'TEST2' as const,
			s,
		};
	},
};
const selectors = {
	testSelector: ( state: number, s: string ) => {
		return s + state;
	},
	testSelector2: ( state: number ) => {
		return state;
	},
};
const resolvers = {
	testSelector: ( s: string ) => async ( {
		dispatch,
	}: {
		dispatch: any; // Could be typed better
	} ) => {
		dispatch.testDispatchAction2( s );
	},
	testSelector2: () => async ( { dispatch }: { dispatch: any } ) => {
		dispatch.testDispatchAction2( 'hello' );
	},
};

type ValueOf< A > = A[ keyof A ];
const reducer = (
	state: number,
	action: ReturnType< ValueOf< typeof actions > >
) => {
	switch ( action.type ) {
		case 'TEST':
			return state + action.n;
		case 'TEST2':
			return state + Number( action.s );
		default:
			return state;
	}
};
function storeConfig(): ReduxStoreConfig<
	number,
	typeof actions,
	typeof selectors
> {
	return {
		reducer,
		actions,
		selectors,
		resolvers, // First error here
	};
}

export interface Stores extends DataStores {
	testStore: ReturnType< typeof storeConfig >;
}

const dr = {} as DataRegistry;
const resolveSelect = await dr.resolveSelect( 'testStore' );
const resolveSelectRes = await resolveSelect.testSelector( 'hello' ); // Second error here
const select = dr.select( 'testStore' );
const selectResult = select.testSelector( 'hello' );
const dispatch = dr.dispatch( 'testStore' );
const dispatchResult = dispatch.testDispatchAction( 42 );
const unsubscribe = dr.subscribe( console.log.bind( console ) );
unsubscribe();
console.log( resolveSelectRes, selectResult, dispatchResult );

Error Message

$ tsc --build

packages/data/src/types.ts:330:3 - error TS2322: Type '{ testSelector: (s: string) => ({ dispatch, }: { dispatch: any; }) => Promise<void>; testSelector2: () => ({ dispatch }: { dispatch: any; }) => Promise<void>; }' is not assignable to type '{ testSelector?: ((s: string) => Promise<string>) | undefined; testSelector2?: (() => Promise<number>) | undefined; }'.
  The types returned by 'testSelector(...)' are incompatible between these types.
    Type '({ dispatch, }: { dispatch: any; }) => Promise<void>' is missing the following properties from type 'Promise<string>': then, catch, finally, [Symbol.toStringTag]

330   resolvers,
      ~~~~~~~~~

  packages/data/src/types.ts:41:2
    41  resolvers?: ResolversOf< Selectors >;
        ~~~~~~~~~
    The expected type comes from property 'resolvers' which is declared here on type 'ReduxStoreConfig<number, { testDispatchAction: (n: number) => { type: "TEST"; n: number; }; testDispatchAction2: (s: string) => { type: "TEST2"; s: string; }; }, { testSelector: (state: number, s: string) => string; testSelector2: (state: number) => number; }>'

packages/data/src/types.ts:340:46 - error TS2339: Property 'testSelector' does not exist on type '{} | Required<{ testSelector?: ((s: string) => Promise<string>) | undefined; testSelector2?: (() => Promise<number>) | undefined; }>'.
  Property 'testSelector' does not exist on type '{}'.

340 const resolveSelectRes = await resolveSelect.testSelector( 'hello' );
                                                 ~~~~~~~~~~~~


Found 2 errors.

I understand that it will make for a larger PR, but I'd like to see these types being used in actual code somewhere. The work is going to need to be done anyway, and it's much easier to test types when they're used and you can click through them in the context of where thy will be used.

Comment on lines +51 to +54
export interface DataStores {}

export interface Stores extends DataStores {}

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of extending Stores from DataStores?

type ResolvedThunks< Actions extends AnyConfig[ 'actions' ] > = {
[ Action in keyof Actions ]: Actions[ Action ] extends (
...args: infer P
) => ( ...thunkArgs: any ) => infer R
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
) => ( ...thunkArgs: any ) => infer R
) => ( ...thunkArgs: any[] ) => infer R

Minor change, but it looks like you're using any[] elsewhere, so it could be used here too.

Comment on lines +47 to +49
export interface Unsubscriber {
(): void;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason that you're using an interface here instead of the more readable type declaration?

Suggested change
export interface Unsubscriber {
(): void;
}
export type Unsubscriber = () => void;

@adamziel
Copy link
Contributor

adamziel commented Jun 6, 2022

@dmsnell I've got these types to work with core-data on my local working copy, but I've also ran into an issue with currying:

// Minimum setup:
interface CommentEntityRecord{ id: number; content: string; }
interface PostEntityRecord{ id: string; title: string; }

type EntityConfig = 
    | { kind: 'root', record: CommentEntityRecord, keyType: number }
    | { kind: 'postType', record: PostEntityRecord, keyType: string }

// Minimal reproduction of getEntityRecords
const getEntityRecords = <K extends EntityConfig['kind']>(
    kind: K,
    key: Extract< EntityConfig, { kind: K } >['keyType']
) : Extract< EntityConfig, { kind: K } >['record'] => { return {} as any; };

const comment = getEntityRecords('root', 15);
//    ^? CommentEntityRecord
// Great! It's what we wanted

const clonedGetEntityRecords = {} as (...args:Parameters<typeof getEntityRecords>) => ReturnType<typeof getEntityRecords>;
const clonedComment = clonedGetEntityRecords('root', 15);
//    ^? CommentEntityRecord | PostEntityRecord
// Oops, we don't want a union type here

// Also:
type Params = Parameters<typeof getEntityRecords>
//   ^ ? [kind: "root" | "postType", key: string | number]
// But we know the signature is more complex than that

It looks like copying a function with interdependent generic arguments in TypeScript is more involved than just using Parameters<> and ReturnValue<> – unfortunately I didn't crack the code yet.

TS Playground

The following SO tackles a similar issue for the ReturnType: https://stackoverflow.com/questions/50321419/typescript-returntype-of-generic-function – I haven't found any for the Parameters<> though.

adamziel added a commit that referenced this pull request Jun 23, 2022
This commit adds TypeScript type definitions to `useSelect` via jsDoc annotations. Effectively, it give us autocompletion support:

https://user-images.githubusercontent.com/205419/172873582-0de54185-7bdd-4f33-9b37-052cba954f5a.mp4

It's a step towards #39081, only it focuses just on `useSelect`

Using the annotations in the same file where they're defined works flawlessly. However, after importing useSelect in another file, autocompletion doesn't work correctly. I believe it's because DefinitelyTyped definition get in the way.  To confirm that hypothesis, I set up a separate repository with clean slate and no Lerna packages. Suddenly, all the  annotations worked flawlessly when imported. I would like to make it work across Gutenberg packages but let's explore that in follow-up PR.
* Add SelectorWithCustomCurrySignature, a utility type to provide a custom curry signature for selectors when TypeScript inference falls short.

* Add the PR link to the docstrings

* Update packages/data/src/types.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants