Skip to content

Conversation

@adamziel
Copy link
Contributor

@adamziel adamziel commented Apr 4, 2022

What?

This is a subset of #39025 – a mega branch that proposes TypeScript signatures for all @wordpress/core-data selectors.

This PR adds Entity configuration types to core-data/src/entities.ts.

Why?

Consider the getEntityRecord selector:

/**
* Returns the Entity's record object by key. Returns `null` if the value is not
* yet received, undefined if the value entity is known to not exist, or the
* entity object if it exists and is received.
*
* @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|undefined} Record.
*/
export const getEntityRecord = createSelector(
( state, kind, name, key, query ) => {

Different entity kinds, like root, postType, taxonomy, are associated with different entity names. For example, kind: root, name: plugin is a valid combination, but kind: postType, name: plugin is not. Other valid combinations are configured in the entities.ts file via a JavaScript object.

An ideal getEntityRecord function signature would only accept valid combinations, then require the key to be either number or a string, and return the list of corresponding entity records.

Again, ideally, there would only be a single source of truth for all the information. I'd rather avoid rewriting them in TypeScript as in my experience this adds maintenance burden, leads to difficult bugs, and gets out of sync sooner or later.

This PR, then, takes all the JavaScript configuration details, and brings them into the TypeScript type system using the as const assertion:

const pluginConfig = {
	label: __( 'Plugins' ),
	name: 'plugin',
	kind: 'root',
	baseURL: '/wp/v2/plugins',
	baseURLParams: { context: 'edit' },
	key: 'plugin',
} as const;

// typeof pluginConfig['name'] is not string, it's "plugin"

Then, it transforms these const types into an Entity Config type like this:

type PluginConfig< C extends Context > = EntityConfigTypeFromConst<
	typeof pluginConfig,
	Records.Plugin< C >
>;
// PluginConfig['kind'] is "root"
// PluginConfig['defaultContext'] is "edit"

That PluginConfig type can then be reused to create a getEntityRecord signature as seen in #39025.

Why have a new config type when the consts could be used directly?

Good question! It's needed because entity configuration comes from three different sources:

  • Hardcoded types in entities.ts
  • Implicit knowledge about the entities loaded using the entity loaders, e.g. that the default context of all kind=postType is edit as seen in loadPostTypeEntities
  • Any additional configuration provided by Gutenberg extenders.

A somewhat eccentric metaphor would be picturing it as a MySQL query that joins three tables:

SELECT
	kind,
	name,
	recordType,
	DEFAULT(key, 'id') as key,
	DEFAULT(baseURLParams['context'], 'view') as defaultContext
FROM rootEntitiesConfig_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_from_additionalEntityConfigLoaders
UNION
SELECT
	kind,
	name,
	recordType,
	key,
	defaultContext
FROM additional_configuration_provided_by_extenders

A common format like the PluginConfig makes reasoning about all these data sources much easier down the road, e.g. it enables the following succinct formulation of the Kind type:

export type KindOf< R extends EntityRecord > = EntityConfigOf< R >[ 'kind' ];

The downside of as const is that it provides no autocompletion or type constraints to the developer writing new types

That's true! It is the price to pay for having a single source of truth. I think it's a worthy trade-off, but there may be a way that enables both using a clever trick I found on StackOverflow:

CleanShot 2022-04-04 at 14 00 07

The downside of that approach is that it requires a few complex types that muddy the big picture. See the related TypeScript playground.

Test plan

Confirm the checks are green and that no entity configuration got changed when I split the large array into atomic declarations. The changes here should only affect the type system so there is nothing to test in the browser.

cc @dmsnell @jsnajdr @youknowriad @sarayourfriend @getdave @draganescu @scruffian

@adamziel adamziel added [Type] Code Quality Issues or PRs that relate to code quality [Package] Core data /packages/core-data Developer Experience Ideas about improving block and theme developer experience labels Apr 4, 2022
@adamziel adamziel requested a review from nerrad as a code owner April 4, 2022 12:03
@adamziel adamziel self-assigned this Apr 4, 2022
adamziel added a commit that referenced this pull request May 5, 2022
…pe signatures (#40025)

Replaces the jsDoc annotations like `@param {Object}` and `@return {string}` in favor of their TypeScript counterparts. This is the first step towards introducing exhaustive type signatures after #40024 is merged.
@adamziel
Copy link
Contributor Author

adamziel commented May 5, 2022

@dmsnell This PR is the critical fork in the road that will determine how will the entity record selectors be typed. Resolving merge conflicts is tedious here so I'll hold off on that until we converge on the approach.

@dmsnell
Copy link
Member

dmsnell commented May 6, 2022

@adamziel do you have any branches or commits lying around where we tried using something like an EntityConfig wrapper? I'd like to put it back and see if we can get all we want by updating to the still-beta release of TypeScript 4.7

if not I'll see if I can build from here. looks like everything is prime finally for playing with this

@dmsnell
Copy link
Member

dmsnell commented May 6, 2022

@adamziel hope you don't mind; I rebased, moving HEAD from f287783 to 9959b2a.

The only real merge conflict I saw was the removal of the navigation area entity; please review my resolution. If there's anything wrong we can reset --hard back to your last commit.

@dmsnell dmsnell force-pushed the ts/core-data-kind-and-name-types branch from f287783 to 9959b2a Compare May 6, 2022 00:44
@adamziel
Copy link
Contributor Author

adamziel commented May 6, 2022

@dmsnell I'm not entirely sure what do you mean, but if you refer to the explorations you've done in #39481 then I don't think we have any branches doubling down on that approach anymore. I think the mega-branch done that at one point, but rebasing got out of hand with the number of intermediary commits so I squashed it and lost that work :(

I went ahead and recreated some of that in the TS Playground. May not be what you're after, though.

@dmsnell
Copy link
Member

dmsnell commented May 6, 2022

that playground link works! thanks.

what I'm after is this: I think we started with the intuition that we would create EntityConfig as a type and then require that all of the actual entity config data is of that type. this derailed us when we realized we can't infer the literal values in those configs unless we declare them as const and remove the universal type constraint.

I want to make sure we don't introduce all the WithLiterals complexity if we can accomplish our original goal in the upcoming release of TypeScript, which is new since we hit hard on entity config.

adamziel added a commit that referenced this pull request May 23, 2022
Add Entity configuration types to core-data/src/entities.ts for the selector types to lean upon in the upcoming PR.

Writing the kind and name twice is a trade-off. I've spent hours exploring the available options with @dmsnell and we concluded that it's only possible to either:

* Infer it from the config and miss out on autocompletion, config type validation, and require using as const. Reuse the JS entities configuration in the TypeScript type system #40024 explored that
* Infer it from the config and have all of the above, but at the cost of using super complex type plumbing. This TS playground explores that
* Type it explicitly, have autocompletion and type validation without complex types, but duplicate a few lines of code.

This commit implements the latter approach.
@adamziel
Copy link
Contributor Author

Closing in favor of the alternative: #40995

@adamziel adamziel closed this May 23, 2022
@johnbillion johnbillion deleted the ts/core-data-kind-and-name-types branch February 10, 2025 16:43
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 [Package] Core data /packages/core-data [Type] Code Quality Issues or PRs that relate to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants