Skip to content

Commit

Permalink
Preserve the generic nature of getEntityRecord and getEntityRecords t…
Browse files Browse the repository at this point in the history
…hrough signature currying in `@wordpress/data`.

Declare GetEntityRecord as a *callable interface* that is callable
as usually, but also ships another signature without the state argument.
This works around a TypeScript limitation that doesn't allow
currying generic functions:

```ts
type CurriedState = F extends ( state: any, ...args: infer P ) => infer R
    ? ( ...args: P ) => R
    : F;

type Selector = <K extends string | number>(
    state: any,
    kind: K,
    key: K extends string ? 'string value' : false
) => K;

type BadlyInferredSignature = CurriedState< Selector >
// BadlyInferredSignature evaluates to:
// (kind: string number, key: false | "string value") => string number
```

The signature without the state parameter shipped as CurriedSignature
is used in the return value of `select( coreStore )`.

See #41578 for more details.

This commit includes a docgen update to add support for typecasting
selectors
  • Loading branch information
adamziel committed Sep 26, 2022
1 parent 7829913 commit 1aa5f76
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 140 deletions.
10 changes: 5 additions & 5 deletions docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,14 @@ Returns the Entity's records.

_Parameters_

- _state_ `State`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID.
- _state_ State tree
- _kind_ Entity kind.
- _name_ Entity name.
- _query_ Optional terms query. If requesting specific fields, fields must always include the ID.

_Returns_

- `EntityRecord[] | null`: Records.
- Records.

### getLastEntityDeleteError

Expand Down
73 changes: 0 additions & 73 deletions packages/autop/README.md
Original file line number Diff line number Diff line change
@@ -1,73 +0,0 @@
# Autop

JavaScript port of WordPress's automatic paragraph function `autop` and the `removep` reverse behavior.

## Installation

Install the module

```bash
npm install @wordpress/autop --save
```

_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._

### API

<!-- START TOKEN(Autogenerated API docs) -->

#### autop

Replaces double line-breaks with paragraph elements.

A group of regex replaces used to identify text formatted with newlines and
replace double line-breaks with HTML paragraph tags. The remaining line-
breaks after conversion become `<br />` tags, unless br is set to 'false'.

_Usage_

```js
import { autop } from '@wordpress/autop';
autop( 'my text' ); // "<p>my text</p>"
```

_Parameters_

- _text_ `string`: The text which has to be formatted.
- _br_ `boolean`: Optional. If set, will convert all remaining line- breaks after paragraphing. Default true.

_Returns_

- `string`: Text which has been converted into paragraph tags.

#### removep

Replaces `<p>` tags with two line breaks. "Opposite" of autop().

Replaces `<p>` tags with two line breaks except where the `<p>` has attributes.
Unifies whitespace. Indents `<li>`, `<dt>` and `<dd>` for better readability.

_Usage_

```js
import { removep } from '@wordpress/autop';
removep( '<p>my text</p>' ); // "my text"
```

_Parameters_

- _html_ `string`: The content from the editor.

_Returns_

- `string`: The content with stripped paragraph tags.

<!-- END TOKEN(Autogenerated API docs) -->

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.

To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md).

<br /><br /><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
10 changes: 5 additions & 5 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,14 +454,14 @@ Returns the Entity's records.

_Parameters_

- _state_ `State`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID.
- _state_ State tree
- _kind_ Entity kind.
- _name_ Entity name.
- _query_ Optional terms query. If requesting specific fields, fields must always include the ID.

_Returns_

- `EntityRecord[] | null`: Records.
- Records.

### getLastEntityDeleteError

Expand Down
99 changes: 85 additions & 14 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,55 @@ export function getEntityConfig(
return find( state.entities.config, { kind, name } );
}

/**
* GetEntityRecord is declared as a *callable interface* with
* two signatures to work around the fact that TypeScript doesn't
* allow currying generic functions:
*
* ```ts
* type CurriedState = F extends ( state: any, ...args: infer P ) => infer R
* ? ( ...args: P ) => R
* : F;
* type Selector = <K extends string | number>(
* state: any,
* kind: K,
* key: K extends string ? 'string value' : false
* ) => K;
* type BadlyInferredSignature = CurriedState< Selector >
* // BadlyInferredSignature evaluates to:
* // (kind: string number, key: false | "string value") => string number
* ```
*
* The signature withot the state parameter shipped as CurriedSignature
* is used in the return value of `dispatch( coreStore )`.
*
* See https://github.com/WordPress/gutenberg/pull/41578 for more details.
*/
export interface GetEntityRecord {
<
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
state: State,
kind: string,
name: string,
key: EntityRecordKey,
query?: GetRecordsHttpQuery
): EntityRecord | undefined;

CurriedSignature: <
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
kind: string,
name: string,
key: EntityRecordKey,
query?: GetRecordsHttpQuery
) => EntityRecord | undefined;
}

/**
* 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
Expand All @@ -236,7 +285,7 @@ export function getEntityConfig(
* @return Record.
*/
export const getEntityRecord = createSelector(
<
( <
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
Expand Down Expand Up @@ -279,7 +328,7 @@ export const getEntityRecord = createSelector(
}

return item;
},
} ) as GetEntityRecord,
( state: State, kind, name, recordId, query ) => {
const context = query?.context ?? 'default';
return [
Expand All @@ -301,7 +350,7 @@ export const getEntityRecord = createSelector(
] ),
];
}
);
) as any as GetEntityRecord;

/**
* Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity records from the API if the entity record isn't available in the local state.
Expand Down Expand Up @@ -414,6 +463,37 @@ export function hasEntityRecords(
return Array.isArray( getEntityRecords( state, kind, name, query ) );
}

/**
* GetEntityRecord is declared as a *callable interface* with
* two signatures to work around the fact that TypeScript doesn't
* allow currying generic functions.
*
* @see GetEntityRecord
* @see https://github.com/WordPress/gutenberg/pull/41578
*/
export interface GetEntityRecords {
<
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
state: State,
kind: string,
name: string,
query?: GetRecordsHttpQuery
): EntityRecord[] | null;

CurriedSignature: <
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
kind: string,
name: string,
query?: GetRecordsHttpQuery
) => EntityRecord[] | null;
}

/**
* Returns the Entity's records.
*
Expand All @@ -425,16 +505,7 @@ export function hasEntityRecords(
*
* @return Records.
*/
export const getEntityRecords = <
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
state: State,
kind: string,
name: string,
query?: GetRecordsHttpQuery
): EntityRecord[] | null => {
export const getEntityRecords = ( ( state, kind, name, query ) => {
// Queried data state is prepopulated for all known entities. If this is not
// assigned for the given parameters, then it is known to not exist.
const queriedState = get( state.entities.records, [
Expand All @@ -446,7 +517,7 @@ export const getEntityRecords = <
return null;
}
return getQueriedItems( queriedState, query );
};
} ) as GetEntityRecords;

type DirtyEntityRecord = {
title: string;
Expand Down
65 changes: 59 additions & 6 deletions packages/data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,75 @@ export type CurriedSelectorsOf< S > = S extends StoreDescriptor<
: never;

/**
* Removes the first argument from a function
* Removes the first argument from a function.
*
* This is designed to remove the `state` parameter from
* By default, it removes the `state` parameter from
* registered selectors since that argument is supplied
* by the editor when calling `select(…)`.
*
* For functions with no arguments, which some selectors
* are free to define, returns the original function.
*
* It is possible to manually provide a custom curried signature
* and avoid the automatic inference. When the
* F generic argument passed to this helper extends the
* SelectorWithCustomCurrySignature type, the F['CurriedSignature']
* property is used verbatim.
*
* This is useful because TypeScript does not correctly remove
* arguments from complex function signatures constrained by
* interdependent generic parameters.
* For more context, see https://github.com/WordPress/gutenberg/pull/41578
*/
export type CurriedState< F > = F extends (
state: any,
...args: infer P
) => infer R
type CurriedState< F > = F extends SelectorWithCustomCurrySignature
? F[ 'CurriedSignature' ]
: F extends ( state: any, ...args: infer P ) => infer R
? ( ...args: P ) => R
: F;

/**
* Utility to manually specify curried selector signatures.
*
* It comes handy when TypeScript can't automatically produce the
* correct curried function signature. For example:
*
* ```ts
* type BadlyInferredSignature = CurriedState<
* <K extends string | number>(
* state: any,
* kind: K,
* key: K extends string ? 'one value' : false
* ) => K
* >
* // BadlyInferredSignature evaluates to:
* // (kind: string number, key: false "one value") => string number
* ```
*
* With SelectorWithCustomCurrySignature, we can provide a custom
* signature and avoid relying on TypeScript inference:
* ```ts
* interface MySelectorSignature extends SelectorWithCustomCurrySignature {
* <K extends string | number>(
* state: any,
* kind: K,
* key: K extends string ? 'one value' : false
* ): K;
*
* CurriedSignature: <K extends string | number>(
* kind: K,
* key: K extends string ? 'one value' : false
* ): K;
* }
* type CorrectlyInferredSignature = CurriedState<MySelectorSignature>
* // <K extends string | number>(kind: K, key: K extends string ? 'one value' : false): K;
*
* For even more context, see https://github.com/WordPress/gutenberg/pull/41578
* ```
*/
export interface SelectorWithCustomCurrySignature {
CurriedSignature?: Function;
}

export interface DataRegistry {
register: ( store: StoreDescriptor< any > ) => void;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/docgen/lib/get-type-annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,18 +405,24 @@ function unwrapWrappedSelectors( token ) {
return token;
}

if ( babelTypes.isTSAsExpression( token ) ) {
// ( ( state, queryId ) => state.queries[ queryId ] ) as any;
// \------------------------------------------------/ CallExpression.expression
return unwrapWrappedSelectors( token.expression );
}

if ( babelTypes.isCallExpression( token ) ) {
// createSelector( ( state, queryId ) => state.queries[ queryId ] );
// \--------------------------------------------/ CallExpression.arguments[0]
if ( token.callee.name === 'createSelector' ) {
return token.arguments[ 0 ];
return unwrapWrappedSelectors( token.arguments[ 0 ] );
}

// createRegistrySelector( ( selector ) => ( state, queryId ) => select( 'core/queries' ).get( queryId ) );
// \-----------------------------------------------------------/ CallExpression.arguments[0].body
// \---------------------------------------------------------------------------/ CallExpression.arguments[0]
if ( token.callee.name === 'createRegistrySelector' ) {
return token.arguments[ 0 ].body;
return unwrapWrappedSelectors( token.arguments[ 0 ].body );
}
}
}
Expand Down
Loading

0 comments on commit 1aa5f76

Please sign in to comment.