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

Convert core-data selectors from jsDoc annotations into TypeScript type signatures #40025

Merged
merged 32 commits into from
May 5, 2022

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 removes 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.

There are two shortcomings for now:

  1. The doc tool that handles START TOKEN(Autogenerated selectors fails. Apparently this is the first instance of mixing it with TypeScript files in the repo. (edit: should be fixed by docgen: Unwrap wrapped selectors when inferring types of JSDoc params #40236)
  2. createRegistrySelector isn't typed at the moment, which means that calling it like const mySelector = createRegistrySelector( select => ( state: State ) => null ) produces a mySelector variable of type any. We could solve it here by adding a .d.ts file to provide the missing signature, or we could solve this problem in @wordpress/data instead.

Testing Instructions

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:11
@adamziel adamziel self-assigned this Apr 4, 2022
@adamziel adamziel requested a review from dmsnell April 4, 2022 12:12
@dmsnell
Copy link
Member

dmsnell commented Apr 4, 2022

I'm confused by this: why create new types out of band instead of adding type annotation to the actual selectors file?

@adamziel
Copy link
Contributor Author

adamziel commented Apr 5, 2022

@dmsnell I just started this one without realizing selectors.js can now be easily renamed to selectors.ts. I had a much harder time doing that last month :-) Let's rebase and apply these changes directly.

@adamziel adamziel force-pushed the ts/core-data-selectors-signatures branch from 0f1e344 to 759d009 Compare April 5, 2022 11:37
@adamziel
Copy link
Contributor Author

adamziel commented Apr 5, 2022

Ah yes, our doc building tool throw an error when the TS signatures are mixed with jsDoc:

Error: Command failed with exit code 1: "core/plugins/gutenberg/node_modules/.bin/docgen" packages/core-data/src/selectors.ts --output docs/reference-guides/data/data-core.md --to-token --use-token "Autogenerated selectors|../../../packages/core-data/src/selectors.ts" --ignore "/unstable|experimental/i"

TypeError: Cannot read property '0' of undefined
    at makeError (core/plugins/gutenberg/node_modules/execa/lib/error.js:59:11)
    at handlePromise (core/plugins/gutenberg/node_modules/execa/index.js:114:26)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async Transform.<anonymous> (core/plugins/gutenberg/bin/api-docs/update-api-docs.js:219:5) {
  shortMessage: 'Command failed with exit code 1: "core/plugins/gutenberg/node_modules/.bin/docgen" packages/core-data/src/selectors.ts --output docs/reference-guides/data/data-core.md --to-token --use-token "Autogenerated selectors|../../../packages/core-data/src/selectors.ts" --ignore "/unstable|experimental/i"',
  command: '"core/plugins/gutenberg/node_modules/.bin/docgen" packages/core-data/src/selectors.ts --output docs/reference-guides/data/data-core.md --to-token --use-token "Autogenerated selectors|../../../packages/core-data/src/selectors.ts" --ignore "/unstable|experimental/i"',
  exitCode: 1,
  signal: undefined,
  signalDescription: undefined,
  stdout: '',
  stderr: "\nTypeError: Cannot read property '0' of undefined",
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}

I'll just convert all the jsDoc annotations into TypeScript ones, then.

@adamziel adamziel changed the title Add a subset of core-data selectors TypeScript signatures Convert core-data selectors from jsDoc annotations into TypeScript type signatures Apr 5, 2022
@adamziel
Copy link
Contributor Author

adamziel commented Apr 5, 2022

It seems like it's not about mixing the jsDoc annotations with the TypeScript syntax. It's about the TypeScript syntax. I converted the entire selectors.ts file to rely on it, yet the error persists. A quick grep tells me this is the first attempt to use autogenerated annotations with a .ts file:

$ git grep 'START TOKEN' | grep '\.ts'
docs/reference-guides/data/data-core.md:<!-- START TOKEN(Autogenerated selectors|../../../packages/core-data/src/selectors.ts) -->
packages/core-data/README.md:<!-- START TOKEN(Autogenerated selectors|src/selectors.ts) -->

@dmsnell
Copy link
Member

dmsnell commented Apr 11, 2022

A quick grep tells me this is the first attempt to use autogenerated annotations with a .ts file:

That thing keeps giving us more and more trouble; @mirka recently raised an issue for React components wanting to add descriptions to arguments inline.

/** Does a thing */
function ({
	/** Name of thing */
	name: string,

	/** How many times you want to do the thing. */
	count: number,
}) => {  }

cc: @sarayourfriend. I'm not sure what to do because I don't want to divert energy to the docs generation, but I feel like this is going to hit us more and more as we move to TypeScript

dmsnell added a commit that referenced this pull request Apr 11, 2022
In #40025 we ran into an obstacle when transitioning code into TypeScript,
namely that our `docgen` tool is unable to find the associated types for
parameters which are documented in JSDoc comments if the parameters are
for the returned function from a call to some wrapping function.

In this patch we're adding two special cases for selectors that call
`createSelector` and `createRegistrySelector` to allow our `docgen` tool
to analyze those inner functions which represent the actual selector.

Fundamentally we should be asking TypeScript for the inferred types of
the function and its parameters but given that we don't have a current
mechanism to do that this issue remains a blocker for broader TypeScript
work. Because of that we're introducing hard-coded special cases for
these common selector wrappers so that we can unblock the TypeScript
work without introducing a generic compromise with potentially-harmful
side-effects, such as might happen if we were to always return the
first argument of a call expression.
@dmsnell
Copy link
Member

dmsnell commented Apr 11, 2022

The main issue with docgen here is that we've decoupled the descriptions from the types and for wrapped functions that means our parameters are kind of hidden inside the call to the wrapper (createSelector and createRegistrySelector).

In #40236 I've proposed a hard-coded solution to docgen until we can rearchitect it to poll TypeScript for the type information. I think it's worth adding special-cases for selectors in the meantime; that would unblock this PR and other selectors.ts changes we'll be making soon.

dmsnell added a commit that referenced this pull request Apr 12, 2022
In #40025 we ran into an obstacle when transitioning code into TypeScript,
namely that our `docgen` tool is unable to find the associated types for
parameters which are documented in JSDoc comments if the parameters are
for the returned function from a call to some wrapping function.

In this patch we're adding two special cases for selectors that call
`createSelector` and `createRegistrySelector` to allow our `docgen` tool
to analyze those inner functions which represent the actual selector.

Fundamentally we should be asking TypeScript for the inferred types of
the function and its parameters but given that we don't have a current
mechanism to do that this issue remains a blocker for broader TypeScript
work. Because of that we're introducing hard-coded special cases for
these common selector wrappers so that we can unblock the TypeScript
work without introducing a generic compromise with potentially-harmful
side-effects, such as might happen if we were to always return the
first argument of a call expression.
dmsnell added a commit that referenced this pull request Apr 15, 2022
…#40236)

In #40025 we ran into an obstacle when transitioning code into TypeScript,
namely that our `docgen` tool is unable to find the associated types for
parameters which are documented in JSDoc comments if the parameters are
for the returned function from a call to some wrapping function.

In this patch we're adding two special cases for selectors that call
`createSelector` and `createRegistrySelector` to allow our `docgen` tool
to analyze those inner functions which represent the actual selector.

Fundamentally we should be asking TypeScript for the inferred types of
the function and its parameters but given that we don't have a current
mechanism to do that this issue remains a blocker for broader TypeScript
work. Because of that we're introducing hard-coded special cases for
these common selector wrappers so that we can unblock the TypeScript
work without introducing a generic compromise with potentially-harmful
side-effects, such as might happen if we were to always return the
first argument of a call expression.
@adamziel adamziel force-pushed the ts/core-data-selectors-signatures branch from 3a37e20 to 85df8a7 Compare April 22, 2022 13:23
@adamziel
Copy link
Contributor Author

adamziel commented Apr 25, 2022

// actual types of `state` and `n` are bound by `Selector<…>`
const getBooks: Selector<Book[] | null, [name: string]> = (state, n) => …

@dmsnell Hm it is quite clever, still I'd rather just call that argument name, otherwise I'm not sure what it means as I read the code.

"If you are writing a selector, use `const mySelector: Selector = " and then have everything auto-complete.

This would be pretty great! And I think we'll get there when integrating the types from @wordpress/core-data with the ones from @wordpress/data. Perhaps that won't be the first iteration, as you mentioned, but I would really like to make these selectors aware of things like:

  • I belong to this store
  • Therefore, the state has a certain type
  • Also, there are these other selectors I can refer to
  • I need to pass the state if I call them directly
  • But I don't need to do that when they're thunks

I don't like using these lengthy infer traps, but I believe that's the way here.

- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _recordId_ `string`: Record's id.
- _recordId_ `RecordKey`: Record's id.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like how it says RecordKey here, I'd rather have it say string | number

Copy link
Member

Choose a reason for hiding this comment

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

why not? because of the auto-generated documentation?

if that's the case I don't think we can care too much about this problem unless and until we radically change our docs. this is going to be a problem with more and more TypeScript code since this is a win for the types but masks the actual type in the docs. we need hyper-links in our docs to jump to and provide a tooltip description for the types like RecordKey here.

unless we get those hyperlinks/tooltips I don't know if there's a good workaround that we should chase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just don't like how it doesn't tell me what a RecordKey is. How do I construct this type as a user of this code? You nailed it here:

we need hyper-links in our docs to jump to and provide a tooltip description for the types like RecordKey here.

Once we have that, we're golden. I don't think it needs to be necessarily hard, too. Perhaps there are even existing documentation tools that do that and that wouldn't be too hard to leverage.

@adamziel
Copy link
Contributor Author

@dmsnell Yay, all checks are finally green here. Thank you so much for your help with the documentation tools! I'd say this is ready to be reviewed.

Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

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

it is quite clever, still I'd rather just call that argument name, otherwise I'm not sure what it means as I read the code.

it wasn't the point of the work, just a side-effect. style preferences aside, some languages offer this as a first-class element to the language as a way of giving more appropriate names to parameters from the outside and inside.

for example,

function compute( $raw_value: value ) {
    $value = clean( $raw_value );
    …
}

Again, not a big deal or the goal, just a side-effect that has its uses.


Things mostly look good here. There's a lot of room for improvement which I think is fine to do in follow-up work.

  • Get rid of Object
  • Cleanup use of RecordKey
  • Expand State
  • Audit any | ? types.

The any | ? types could honestly go in now before merging and it would be good. I get the purpose of any | null (or at least I can think of a reasonable use-case) but I don't think it warrants the oddity in the type. A stop-gap could be the use of a nullable type like Maybe or Optional.

type Optional<T> = T | null;

And with that we still get any | null if we do Optional<any> but at least we can see that it was intentional and not an oversight.

The state parameter in there for docgen is the other thing I'd like to double-check before merge. Is that still necessary? If it is then please go ahead and merge; if it isn't let's get that out before merging.

Either way, thanks for moving on this very large task. Can't wait until we're getting to the even-more exciting bits.

docs/reference-guides/data/data-core.md Outdated Show resolved Hide resolved
packages/core-data/README.md Outdated Show resolved Hide resolved
packages/core-data/src/selectors.ts Outdated Show resolved Hide resolved
packages/core-data/src/selectors.ts Show resolved Hide resolved
packages/core-data/src/selectors.ts Outdated Show resolved Hide resolved
packages/core-data/src/selectors.ts Show resolved Hide resolved
Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

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

Forgot to mark as approved in my submission…

@adamziel adamziel force-pushed the ts/core-data-selectors-signatures branch from 0fb2ada to 63274af Compare May 5, 2022 13:24
@adamziel
Copy link
Contributor Author

adamziel commented May 5, 2022

it wasn't the point of the work, just a side-effect. style preferences aside, some languages offer this as a first-class element to the language as a way of giving more appropriate names to parameters from the outside and inside.

Thank you for explaining, I misunderstood your intention. It is a nice feature for sure, and it could come handy for our work here.

Also, I've addressed all your notes, included the additional State types – let's get this one in and address any follow-up work in a follow-up PR.

@adamziel adamziel merged commit 4c38108 into trunk May 5, 2022
@adamziel adamziel deleted the ts/core-data-selectors-signatures branch May 5, 2022 14:18
@github-actions github-actions bot added this to the Gutenberg 13.3 milestone May 5, 2022
dmsnell added a commit that referenced this pull request May 5, 2022
…ndants

In #40025 we ran into a situation where a `createSelector()` selector is
building an opaque value memoized on state values as dependencies. While
this is an unexpected use of a selector it's legitimate and required leaving
some additional code and an explanatory comment in order to avoid breaking
the `docgen` process.

In this patch we're adding recognition for that second argument to the
`createSelector()` function, the `getDependants()` function, and if that
function has more parameters than the `selector` itself we'll cheat
and act like its parameters were listed on the selector. This will
likely only happen in practice when the selector ignores `state` but
it's pluasible someone might go further and use other inputs in the
dependency selection but ignore them on the actual state creation.
dmsnell added a commit that referenced this pull request May 6, 2022
…ndants

In #40025 we ran into a situation where a `createSelector()` selector is
building an opaque value memoized on state values as dependencies. While
this is an unexpected use of a selector it's legitimate and required leaving
some additional code and an explanatory comment in order to avoid breaking
the `docgen` process.

In this patch we're adding recognition for that second argument to the
`createSelector()` function, the `getDependants()` function, and if that
function has more parameters than the `selector` itself we'll cheat
and act like its parameters were listed on the selector. This will
likely only happen in practice when the selector ignores `state` but
it's pluasible someone might go further and use other inputs in the
dependency selection but ignore them on the actual state creation.
*/
export function getCurrentUser( state ) {
export function getCurrentUser( state: State ): User< 'edit' > {
Copy link
Member

Choose a reason for hiding this comment

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

The context here should be 'view' instead of 'edit', otherwise it will set the wrong types/properties which are not available in current user object, for example, user.capabilities.

Copy link
Member

Choose a reason for hiding this comment

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

Created #68045 to fix it.

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