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

TypeScript signatures for core-data selectors #39025

Closed
wants to merge 36 commits into from

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Feb 23, 2022

Description

This PR proposes a set of type signatures for selectors in core-data.

Because of the sheer volume of changes, I'm splitting this large PR into a series of smaller, atomic changes:

Overall, the challenges here can be illustrated using getEntityRecord as an example:

* @param {Object} state State tree
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {number} key Record's key
* @param {?Object} query Optional query.
*
* @return {Object?} Record.
*/
export const getEntityRecord = createSelector(
( state, kind, name, key, query ) => {

And the type signature proposed in this PR:

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

Entity record types are associated with string-based kind and name. Type inference is enabled through CoreEntity, a database-disguised-as-a-lookup-type that enables looking up (kind,name)->recordType and vice versa.

Typing the entire core-data is way beyond this PR. This proposal paves the way by adding the appropriate type declarations to src/types/selectors.ts without bringing them over to src/selectors.js yet.

Test plan

Create a scratch file inside core-data and confirm the following data types check out:

const commentDefault = getEntityRecord( {}, 'root', 'comment', 15 );
// commentDefault is Comment<'edit'>

const commentView = getEntityRecord( {}, 'root', 'comment', 15, {
	context: 'view',
} );
// commentView is Comment<'view'>

const commentInvalidPK = getEntityRecord( {}, 'root', 'comment', '15' );
// commentInvalidPK shows a TypeScript error

const commentCustom = getEntityRecord<Comment, 'view'>({}, 'root', 'comment',15, { context: 'view' });
// commentCustom is Comment<'view'>

The R generic is the entity record type, and is the primary mean of configuring the calls to getEntityRecord. The C, K, and N generics would ideally be removed, but I don't see an easy way to do that.

cc @dmsnell @jsnajdr @sarayourfriend @sirreal

@adamziel adamziel added [Package] Core data /packages/core-data Needs Technical Feedback Needs testing from a developer perspective. Developer Experience Ideas about improving block and theme developer experience labels Feb 23, 2022
@adamziel adamziel changed the title Propose type signatures for core-data selectors TypeScript signatures for core-data selectors Feb 23, 2022
@github-actions
Copy link

github-actions bot commented Feb 23, 2022

Size Change: -299 B (0%)

Total Size: 1.15 MB

Filename Size Change
build/core-data/index.min.js 13.8 kB -299 B (-2%)
ℹ️ 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 144 kB
build/block-editor/style-rtl.css 14.8 kB
build/block-editor/style.css 14.8 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 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 965 B
build/block-library/blocks/gallery/editor.css 967 B
build/block-library/blocks/gallery/style-rtl.css 1.61 kB
build/block-library/blocks/gallery/style.css 1.61 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 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 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 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 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 99 B
build/block-library/blocks/separator/editor.css 99 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 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.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 9.92 kB
build/block-library/editor.css 9.92 kB
build/block-library/index.min.js 168 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 11.4 kB
build/block-library/style.css 11.4 kB
build/block-library/theme-rtl.css 665 B
build/block-library/theme.css 670 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.4 kB
build/components/index.min.js 217 kB
build/components/style-rtl.css 15.6 kB
build/components/style.css 15.6 kB
build/compose/index.min.js 11.2 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.05 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 41.9 kB
build/edit-site/style-rtl.css 7.44 kB
build/edit-site/style.css 7.42 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.39 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/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

@adamziel adamziel self-assigned this Feb 23, 2022
@dmsnell
Copy link
Member

dmsnell commented Feb 23, 2022

Whenever I'm working on complicated types like this I like to watch my own code and check for an abundance of special-casing and awkward conventions and if I see that, think about if I'm pushing too hard on approach vs. stepping back to check if there might be others.

We have two really obvious approaches with getEntityRecord:
- auto-type the entire thing with no type annotations necessary on the part of the callers
- rely on manually providing the type of the returned object

In this PR we're shooting for the first one, but obviously there's a ton of machinery in there to make it work, and some remaining questions about the ease of extending it. There are questions about the relationship between kind and name (or maybe those are obvious and it's just my ignorance making me wonder). The types can be a maze, and you found a way through the maze to get the desired output, and bravo for that 👏. Still, it's a winding path with lots of type helpers and wrappers.

The other approach is almost too simple to warrant discussion.

export const getEntityRecord = <T extends EntityRecord<any>>(): T | null => { … }

const post = getEntityRecord<Post<'edit'>>('postType', 'post', 13, { context: 'edit' } );

Every question about extending this is already taken care of because it's up to you to supply the return type (as the caller).

I hope that we can get to full auto-typing and inference, but what I'm wondering is if there's a way to step there that starts with the simpler approach, specifically of having the EntityRecord<any> type parameter as the first parameter. That would let us move upwards to more inference; in the other direction though, it would be awkward to have to write this if we had to give up and step down to the manual approach:

const post = getEntityRecord<'postType', 'post', 13, 'edit'>('postType', 'post', 13, { context: 'edit' } );

Is this possible to try and look for a type signature for full inference that starts with T extends EntityRecord<C>? I don't know. Maybe if not there's at least <C extends Content, T extends EntityRecord<C>>

export const getEntityRecord = <C extends Context, T extends EntityRecord<C>>(
	state: State,
	kind: 
)

For example, I believe that this gets us the expected kind given an entity type.

type KindOf<T extends EntityRecord<any>> = keyof OmitNevers<{
	[Kind in keyof EntityRecordLookup['root']]:
		EntityRecordLookup['root'][Kind] extends T
			? T
			: never;
}>;

Screen Shot 2022-02-23 at 12 50 44 PM

The great thing about extensibility is that if we can get to the point where this is parameterized by one required type parameter, we should be able to provide automatic inference for all the core types without imposing any difficult steps on people to extend it. Maybe they don't want to go overwrite ambient namespaces types; they can supply their own type parameter and call it a day. We can potentially do this through conditional types checking if the given EntityRecord is known or not.

export const getEntityRecord = <T extends EntityRecord<any> | CustomEntityRecord>(): T | null { … }

Screen Shot 2022-02-23 at 12 56 54 PM

Screen.Recording.2022-02-23.at.1.00.43.PM.mov

Lots of doodling, but I'm just trying to see how far we can get with how little in the terms of type parameters and annotation.

@adamziel
Copy link
Contributor Author

adamziel commented Feb 23, 2022

My brain is fried so just a quick note – I think the full inference works (although it's based on kind and type)!

CleanShot 2022-02-23 at 22 27 15
CleanShot 2022-02-23 at 22 27 24

And the types aren't even that complicated:

export type EntityType< C extends Context = any > =
	| DeclaredEntity< 'root', 'site', Settings< C > >
	| DeclaredEntity< 'root', 'postType', Type< C > >
	| DeclaredEntity< 'root', 'media', Attachment< C > >
	| DeclaredEntity< 'root', 'taxonomy', Taxonomy< C > >
	| DeclaredEntity< 'root', 'sidebar', Sidebar< C > >
	| DeclaredEntity< 'root', 'widget', Widget< C > >
	| DeclaredEntity< 'root', 'widgetType', WidgetType< C > >
	| DeclaredEntity< 'root', 'user', User< C > >
	| DeclaredEntity< 'root', 'comment', Comment< C > >
	| DeclaredEntity< 'root', 'menu', NavMenu< C > >
	| DeclaredEntity< 'root', 'menuItem', NavMenuItem< C > >
	| DeclaredEntity< 'root', 'menuLocation', MenuLocation< C > >
	| DeclaredEntity< 'root', 'navigationArea', NavigationArea< C > >
	| DeclaredEntity< 'root', 'theme', Theme< C > >
	| DeclaredEntity< 'root', 'plugin', Plugin< C > >
	| APIEntity< 'postType', 'post', Post< C > >
	| APIEntity< 'postType', 'page', Page< C > >
	| APIEntity< 'postType', 'wp_template', WpTemplate< C > >
	| APIEntity< 'postType', 'wp_template_part', WpTemplatePart< C > >;

export type Kind = EntityType[ 'kind' ];
export type Name = EntityType[ 'name' ];

export type EntityRecordType<
	K extends Kind,
	N extends Name,
	C extends Context = any
> = Extract< EntityType< C >, { kind: K; name: N } >[ 'recordType' ];

export type Key< K extends Kind, N extends Name > = Extract<
	EntityType,
	{ kind: K; name: N }
>[ 'keyType' ];

export type DefaultContext< K extends Kind, N extends Name > = Extract<
	EntityType,
	{ kind: K; name: N }
>[ 'defaultContext' ];

My brain is fried so that's all I'm going to write right now, but my next comment will address the specific points you brought up @dmsnell

--

edit:

Here's the full EntityType-based inference:

type Context = 'edit' | 'update' | 'view' | 'embed';

interface Post < C extends Context > { brand: 'post' }
interface Comment < C extends Context > { author: 2 }
interface Template < C extends Context > { distinct: 84 }
type EntityRecord < C extends Context > = Post<C> | Comment<C> | Template<C>
type State = any;

type EntityType< C extends Context > =
	| { kind : 'root', name: 'post', recordType: Post<C> }
	| { kind : 'root', name: 'comment', recordType: Comment<C> }
	| { kind : 'postType', name: 'template', recordType: Template<C> }

export type RecordOf< Kind, Name, C extends Context > = Extract<
	EntityType<C>,
	{ kind: Kind, name: Name }
	>['recordType']
export type KindOf< R extends EntityRecord<any> > = Extract<
	EntityType<any>,
	{ recordType: R }
	>[ 'kind' ];
export type NameOf< R extends EntityRecord<any> > = Extract<
	EntityType<any>,
	{ recordType: R }
	>[ 'name' ];

export function getEntityRecord<
	R extends RecordOf< Kind, Name, C >,
	C extends Context='edit',
	Kind extends EntityType<C>['kind']=KindOf<R>,
	Name extends EntityType<C>['name']=NameOf<R>
	>(
	state: State,
	kind: Kind,
	name: Name,
	query?: {
		context: C
	}
) : R { return {} as any; }

const post1 = getEntityRecord({}, 'root', 'post')
const post2 = getEntityRecord({}, 'root', 'post', { context: 'view' })
const post3 = getEntityRecord<Post<'edit'>>({}, 'root', 'post')
const post4 = getEntityRecord<Post<'view'>, 'view'>({}, 'root', 'post', { context: 'view' })

Playground link

packages/core-data/src/wordpress-data.d.ts Outdated Show resolved Hide resolved
packages/core-data/tsconfig.json Outdated Show resolved Hide resolved
packages/core-data/tsconfig.json Outdated Show resolved Hide resolved
@adamziel
Copy link
Contributor Author

I reduced this PR to focus just on the getEntityRecord part of it.

The other approach is almost too simple to warrant discussion.

export const getEntityRecord = <T extends EntityRecord<any>>(): T | null => { … }

const post = getEntityRecord<Post<'edit'>>('postType', 'post', 13, { context: 'edit' } );

Great point! This would "just work" and solve a ton of problems here.

The full inference is just so tempting, though 😆 I reduced this PR to something much simpler than it used to be, combining the first version with your explorations, which enabled the following signature:

export const getEntityRecord = function <
  R extends EntityRecordType< K, N, C >,
  K extends Kind = KindOf< R >,
  N extends Name = NameOf< R >,
  C extends Context = DefaultContextOf< K, N >
>(
  state: State,
  kind: K,
  name: N,
  key: PrimaryKey< R >,
  query?: EntityQuery< C >
): R | null | undefined 

Unfortunately, I couldn't the inference to work right without the K and N generic parameters. This how it is used:

const commentDefault = getEntityRecord( {}, 'root', 'comment', 15 );
// commentDefault is Comment<'edit'>

const commentView = getEntityRecord( {}, 'root', 'comment', 15, {
	context: 'view',
} );
// commentView is Comment<'view'>

const commentInvalidPK = getEntityRecord( {}, 'root', 'comment', '15' );
// commentInvalidPK shows a TypeScript error

const commentCustom = getEntityRecord< Comment< 'edit' > >( {}, 'root', 'comment', 15 );
// commentCustom is Comment<'edit'>

const commentCustomContext = getEntityRecord<
	Comment< 'view' >,
	'root',
	'comment',
	'view'
>( {}, 'root', 'comment', 15, {
	context: 'view',
} );
// commentCustom is Comment<'view'>

It requires the K and N parameters when you want to pass a custom Record type in a non-edit context.

I'm not sure the R generic type can be simplified to <T extends EntityRecord<any>> – we need something to somehow tie it to the kind and name function arguments and I couldn't get it to work like this:

// No bueno in either case
export const getEntityRecord = function <
	R extends EntityRecord< C >,
	C extends Context = 'edit'
>(
	state: State,
	kind: KindOf< R >,
	name: NameOf< R >,
	key: PrimaryKey< R >,
	query?: EntityQuery< C >
): R

export const getEntityRecord = function <
	R extends EntityRecord< C >,
	C extends Context = 'edit'
>(
	state: State,
	kind: KindOf< R >,
	name: NameOf< R >,
	key: PrimaryKey< R >,
	query?: EntityQuery< C >
): EntityRecordType< KindOf< R >, NameOf< R >, C > 

cc @dmsnell @sarayourfriend

@sarayourfriend
Copy link
Contributor

The full inference is just so tempting, though laughing I reduced this PR to something much simpler than it used to be, combining the first version with your explorations, which enabled the following signature:

Keeping in mind that most WordPress contributors will probably be consuming this from regular JS and not have the ability to pass type parameters to functions, full inference would be a good goal to reach; we don't even have full TS support in the create-block generator yet.

@adamziel
Copy link
Contributor Author

adamziel commented Feb 24, 2022

I boiled the generics signature down to:

<
    R extends RecordOf< Kind, Name, C >,
    C extends Context=RContextOf<R>,
    Kind extends EntityType<C>['kind']=KindOf<R>,
    Name extends EntityType<C>['name']=NameOf<R>,
>

Which is close to perfection – the record comes first, then the optional context, and then optional Kind and Type which can almost always be omitted. Here's the usage:

const post1 = getEntityRecord({}, 'root', 'post')
// post1 is Post<'edit'>

const post2 = getEntityRecord({}, 'root', 'post', { context: 'view' })
// post2 is Post<'view'>

const post3 = getEntityRecord<Post<'edit'>>({}, 'root', 'post')
// post3 is Post<'edit'>

const post4 = getEntityRecord<Post<'view'>, 'view'>({}, 'root', 'post', { context: 'view' })
// post4 is Post<'view'>

Playground link

One downside I see is that the R parameter is not a EntityRecord< C > but RecordOf< Kind, Name, C >. They coincide, but EntityRecord is a type union while RecordOf is sourced from a different union:

type EntityRecord < C extends Context > = Post<C> | Comment<C> | Template<C>

type EntityType< C extends Context > = 
    | { kind : 'root', name: 'post', recordType: Post<C>, defaultContext: 'edit' }
    | { kind : 'root', name: 'comment', recordType: Comment<C>, defaultContext: 'view' }
    | { kind : 'postType', name: 'template', recordType: Template<C>, defaultContext: 'view' }

// RecordOf depends on EntityType

And this not extensible yet. We can fix that, though, by storing the entity types in an interface as below:

// core-data code
type CoreEntityType< C extends Context > = 
    | { kind : 'root', name: 'post', recordType: Post<C>, defaultContext: 'edit' }
    | { kind : 'root', name: 'comment', recordType: Comment<C>, defaultContext: 'view' }
    | { kind : 'postType', name: 'template', recordType: Template<C>, defaultContext: 'view' }

export interface EntityTypeWrapper< C extends Context > {
    core: CoreEntityType< C >
}

// Same as below, but now we read all the values stored in EntityTypeWrapper
type EntityType< C extends Context > = EntityTypeWrapper<C> [ keyof EntityTypeWrapper<C> ]

// This is no longer a hardcoded union type, but depends on the configuration
type EntityRecord < C extends Context > = EntityType<C>['recordType']

// Plugin code
interface Order { customerId: number; }

type myPluginEntityType< C extends Context > = 
    | { kind : 'myPlugin', name: 'order', recordType: Order, defaultContext: 'view' }

export interface EntityTypeWrapper< C extends Context > {
    myPlugin: myPluginEntityType< C >
}

const order = getEntityRecord({}, 'myPlugin', 'order')
// order is Order

In this case, RecordOf< Kind, Name, C > extends EntityRecord< C > which is what we're after, I believe.

One thing this setup doesn't allow is completely arbitrary return types as in getEntityRecord<NotEntityRecord<'edit'>({}, 'myPlugin', 'order') but I think that's fine.

@dmsnell
Copy link
Member

dmsnell commented Feb 24, 2022

because I have a bunch of changes in my local working copy I'm not going to jump into this now but will come back to it in a bit. I like where it's going; I want to see if I can figure out a way to get rid of the Kind and Name parameters entirely and I am 20% confident we can.

regardless this is going to be great and I'm glad we're pushing hard on some complicated internal types because of the way it will let plugin authors and core contributors outside of this package get all the type benefits with little to no annotation or work on their part.

@sarayourfriend
Copy link
Contributor

export interface EntityTypeWrapper< C extends Context > {
    core: CoreEntityType< C >
}

// Same as below, but now we read all the values stored in EntityTypeWrapper
type EntityType< C extends Context > = EntityTypeWrapper<C> [ keyof EntityTypeWrapper<C> ]

This is super clever, I would not have considered this approach at all. Definitely putting that one in my back pocket.

@adamziel
Copy link
Contributor Author

adamziel commented Feb 25, 2022

This is where we stand right now:

export const getEntityRecord = createSelector(
	function <
		R extends RecordOf< K, N >,
		C extends Context = DefaultContextOf< R >,
		K extends Kind = KindOf< R >,
		N extends Name = NameOf< R >
	>(
		state: State,
		kind: K,
		name: N,
		key: KeyOf< R >,
		query?: EntityQuery< C >
	): RecordOf< K, N, C > | null | undefined {
	// ...
);

const commentDefault = getEntityRecord( {}, 'root', 'comment', 15 );
// commentDefault is Comment<'edit'>

const commentView = getEntityRecord( {}, 'root', 'comment', 15, {
	context: 'view',
} );
// commentView is Comment<'view'>

const commentInvalidPK = getEntityRecord( {}, 'root', 'comment', '15' );
// commentInvalidPK shows a TypeScript error

const commentCustom = getEntityRecord<Comment<'view'>, 'view'>({},'root','comment',15,{ context: 'view' });
// commentCustom is Comment<'view'>

Any big-picture improvements you see? If we're in the right ballpark, I'll start cleaning this PR up.

@dmsnell
Copy link
Member

dmsnell commented Feb 25, 2022

Still having a pretty hard time following the types around here and figuring out the purpose of each of them and how exactly to think through making changes or maintaining them.

I wonder if there's not a different approach that doesn't require so many layers of type wrapping. For instance, when trying to understand DefaultContextOf inside of the type for getEntityRecord I have to unwrap the following layers:

  • DefaultContextOf: okay this just finds a lookup entity record type in a metadata type and returns the defaultContext. Where's that stored?
  • Entity: seems to make sense, I assume it's EntityRecord, but it's not. It's PerPackageEntity, huh, okay, more metadata databasing.
  • PerPackageEntity is a think with core that I guess is for extensibility
  • CoreEntity: we should be getting here, but oh no, there are two types of types here and they don't stem from the same base type. I guess I'll try and ignore what APIEntity might be
  • DeclaredEntity: This has some type magic to lookup an entity config in an actual value defaultEntities. I thought we were already past this lookup but it's happening again now on the value side of the equation. In any case it defines a defaultContext and sets the default value to view, which I thought should have been edit.

Maybe the type that CoreEntity plays could be a nested object instead of a type union?

If we're making an EntityConfig type anyway, can we not use that on defaultEntities and get what we want without the type wrapper?

Going to continue considering this. I'm a bit nervous about it the way it is at the moment, knowing we'll have to support it.

As always, thanks for your persistence and hard work on this!

@adamziel adamziel force-pushed the ts/type-getentityrecords branch from 3c9d87f to b9eb573 Compare February 28, 2022 14:22
@adamziel
Copy link
Contributor Author

adamziel commented Feb 28, 2022

I wonder if there's not a different approach that doesn't require so many layers of type wrapping. For instance, when trying to understand DefaultContextOf inside of the type for getEntityRecord I have to unwrap the following layers:

@dmsnell You've definitely got a point here. I updated this PR to minimize the indirection as much as I could. I also made a distinction between entity records and entity types and added some nudges in the docstrings. I am not sure if this can be simplified much further besides shuffling things around.

If we're making an EntityConfig type anyway, can we not use that on defaultEntities and get what we want without the type wrapper?

Using defaultContext as an example, we have three data sources to consult:

  • defaultEntities declared in entities.js
  • The knowledge we have about the post, page, wp_template, and wp_template_part entities that are not a part of defaultEntities
  • Any additional information from the extenders

To make things more complex, defaultEntities is a value so the kind and type is not tied to the respective record type. For example, the following entry is a value and does not refer the MenuLocation record type:

{
	name: 'menuLocation',
	kind: 'root',
	baseURL: '/wp/v2/menu-locations',
	baseURLParams: { context: 'edit' },
	plural: 'menuLocations',
	label: __( 'Menu Location' ),
	key: 'name',
}

In terms of databases, we are compiling the following data:

SELECT
	kind,
	name,
	recordType,
	DEFAULT(key, 'id') as key,
	DEFAULT(baseURLParams['context'], 'view') as defaultContext
FROM defaultEntities_declared_in_entities_js d
INNER JOIN record_types r ON d.kind = r.kind AND d.name = r.name
UNION
SELECT VALUES
	ROW ( 'postType', 'post', 'id', 'Post', 'edit' ),
	ROW ( 'postType', 'page', 'id', 'Page', 'edit' ),
	ROW ( 'postType', 'wp_template', 'WpTemplate', 'id', 'edit' ),
	ROW ( 'postType', 'wp_template_part', 'WpTemplatePart', 'id', 'edit' ),
AS what_we_intrinsically_know_about_these_unconfigured_entites
UNION
SELECT
	kind,
	name,
	recordType,
	key,
	defaultContext
FROM additional_entities_provided_by_extenders

This is quite a complex query and the underlying TS implementation reflects that complexity.

Maybe the type that CoreEntity plays could be a nested object instead of a type union?

Do you mean something like below?

type CoreEntity = {
	root: {
		comment: Comment<C>,
		// ...
	}
}

That was my first instinct, too. I tried some variations of that, but it generated some additional hoops to jump through and the code ended up being more complex, not less.

I've also tried a few things like redeclaring the key and defaultContext in type definitions instead of sourcing them directly from entities.js, but then editing the configuration caused an unintuitive type errors along the lines of

that context you just changed to view must be of type edit

@adamziel adamziel force-pushed the ts/type-getentityrecords branch from 4752f0a to 27db5ca Compare March 14, 2022 13:23
@adamziel
Copy link
Contributor Author

@dmsnell I rebased this on top of latest trunk, standardized the language a bit, and removed a few unused types.

dmsnell added a commit that referenced this pull request Mar 31, 2022
In this commit we're cleaning up type issues in the core-data package
that prevent us from telling TypeScript to run on the package and all
of its existing code, even the JS files.

After these changes we should be able to do so and start converting
more modules to TypeScript with less friction.

This patch follows a series of other smaller updates:
 - #39212
 - #39214
 - #39225
 - #39476
 - #39477
 - #39479
 - #39480
 - #39525
 - #39526
 - #39655
 - #39656
 - #39659

It was built in order to support ongoing work to add types to the
`getEntityRecord` family of functions in #39025.
dmsnell added a commit that referenced this pull request Apr 1, 2022
In this commit we're cleaning up type issues in the core-data package
that prevent us from telling TypeScript to run on the package and all
of its existing code, even the JS files.

After these changes we should be able to do so and start converting
more modules to TypeScript with less friction.

This patch follows a series of other smaller updates:
 - #39212
 - #39214
 - #39225
 - #39476
 - #39477
 - #39479
 - #39480
 - #39525
 - #39526
 - #39655
 - #39656
 - #39659

It was built in order to support ongoing work to add types to the
`getEntityRecord` family of functions in #39025.
adamziel added a commit that referenced this pull request Jun 4, 2022
…41235)

A subset of #39025. Adds type signatures for the getEntityRecord and getEntityRecords selectors that supports the following use-cases:

```ts
const commentDefault = getEntityRecord( {} as State, 'root', 'comment', 15 );
// commentDefault is Comment<'edit'>

const commentView = getEntityRecord( {} as State, 'root', 'comment', 15, {
	context: 'view',
} );
// commentView is Comment<'view'>

const commentInvalidPK = getEntityRecord(
	{} as State,
	'root',
	'comment',
	'15'
);
// commentInvalidPK shows a TypeScript error

const commentView2 = getEntityRecord( {} as State, 'root', 'comment', 15, {
	context: 'view',
	_fields: [ 'id' ],
} );
// commentView is Partial< Comment<'view'> >
```
@adamziel
Copy link
Contributor Author

This huge PR was entirely merged in small pieces listed in the description above. @wordpress/core-data selectors are now typed!

To make use of these type signatures useful outside of core-data, we'll also need to have a type signature for useSelect. Let's track this work separately – we have two PRs striving to do just that:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Developer Experience Ideas about improving block and theme developer experience Needs Technical Feedback Needs testing from a developer perspective. [Package] Core data /packages/core-data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants