Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eb2ded5
Quote block: stop slash inserter popup showing in citation (#44634)
glendaviesnz Oct 3, 2022
9d1fe96
Query Loop: Fix condition for displaying 'parents' control (#44630)
Mamaduka Oct 3, 2022
7e9fd39
Hide the Classic block in the Site Editor (#44554)
michalczaplinski Sep 30, 2022
1bbc3ab
Fix navigation block console error (#44594)
talldan Sep 30, 2022
50bb90d
Remove the invalid spacing preset setting (#44555)
glendaviesnz Sep 30, 2022
2159bbb
Add box-sizing. (#44580)
ndiego Sep 29, 2022
14d6500
Remove border from Global Styles previews (#44556)
ndiego Sep 29, 2022
ad37bdd
Spacing presets: Modify the styling of the input controls when in unl…
glendaviesnz Sep 29, 2022
4a0fbe0
[TypeScript] Preserve the generic signature of getEntityRecord and ge…
adamziel Sep 27, 2022
4b92996
Theme.json: fix some outline properties doesn't work properly on the …
t-hamano Sep 28, 2022
af3f84e
Add style-engine to gutenberg tsconfig references (#44516)
Sep 27, 2022
9aaf066
[Block Library - Query Loop]: Rename Query Loop variations `allowCont…
ntsekouras Sep 28, 2022
32de2fb
Post Featured Image: Fix borders after addition of overlay feature (#…
aaronrobertshaw Sep 27, 2022
61a1bc2
Template editor: fix crashes due to undefined vars (#44482)
glendaviesnz Sep 28, 2022
add234f
Template part: prevent adding block in post editor or inside post tem…
ramonjd Sep 27, 2022
33f9e9f
Fix rotated image crop area aspect ratio (#44425)
Sep 27, 2022
1876d24
Fix padding/margin visualizer accuracy (#44485)
talldan Sep 27, 2022
a3e9d0a
PostFeaturedImage: Fix application of default border style in editor …
aaronrobertshaw Sep 28, 2022
c5b3727
Theme.json: fix some shadow properties doesn't work properly on the s…
t-hamano Sep 29, 2022
d4cb2cc
ToggleGroupControl: fix unselected icon color (#44575)
ciampo Sep 29, 2022
b3343b9
TokenInput field: try alternative approach to fix screen reader focus…
ciampo Sep 29, 2022
d8fcfec
Add layout styles from Post Content block to post editor (#44258)
tellthemachines Sep 27, 2022
baa41ca
Edit Post: Optimize legacy post content layout (#44506)
tyxla Sep 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[TypeScript] Preserve the generic signature of getEntityRecord and ge…
…tEntityRecords through currying (#44453)

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 authored and michalczaplinski committed Oct 3, 2022
commit 4a0fbe0222d5965721931adb5e3a9294966cf25e
92 changes: 86 additions & 6 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 without the state parameter shipped as CurriedSignature
* is used in the return value of `select( 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 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,15 +505,15 @@ export function hasEntityRecords(
*
* @return Records.
*/
export const getEntityRecords = <
export const getEntityRecords = ( <
EntityRecord extends
| ET.EntityRecord< any >
| Partial< ET.EntityRecord< any > >
>(
state: State,
kind: string,
name: string,
query?: GetRecordsHttpQuery
query: GetRecordsHttpQuery
): EntityRecord[] | null => {
// 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.
Expand All @@ -446,7 +526,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
77 changes: 42 additions & 35 deletions packages/docgen/test/get-type-annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,46 +394,53 @@ describe( 'Type annotations', () => {
} );

describe( 'statically-wrapped function exceptions', () => {
it( 'should find types for inner function with `createSelector`', () => {
const { tokens } = engine(
'test.ts',
`/**
* Returns the number of things
*
* @param state - stores all the things
*/
export const getCount = createSelector( ( state: string[] ) => state.length );
`
const getStateArgType = ( code ) => {
const { tokens } = engine( 'test.ts', code );
return getTypeAnnotation(
{ tag: 'param', name: 'state' },
tokens[ 0 ],
0
);
};

expect(
getTypeAnnotation(
{ tag: 'param', name: 'state' },
tokens[ 0 ],
0
)
).toBe( 'string[]' );
const docString = `/**
* Returns the number of things
*
* @param state - stores all the things
*/`;
it( 'should find types for a typecasted function', () => {
const code = `${ docString }
export const getCount = ( state: string[] ) => state.length;
`;
expect( getStateArgType( code ) ).toBe( 'string[]' );
} );

it( 'should find types for inner function with `createRegistrySelector`', () => {
const { tokens } = engine(
'test.ts',
`/**
* Returns the number of things
*
* @param state - stores all the things
*/
export const getCount = createRegistrySelector( ( select ) => ( state: number ) => state );
`
);
it( 'should find types for a doubly typecasted function', () => {
const code = `${ docString }
export const getCount = ( ( state: string[] ) => state.length ) as any as any;
`;
expect( getStateArgType( code ) ).toBe( 'string[]' );
} );

expect(
getTypeAnnotation(
{ tag: 'param', name: 'state' },
tokens[ 0 ],
0
)
).toBe( 'number' );
it( 'should find types for inner function with `createSelector`', () => {
const code = `${ docString }
export const getCount = createSelector( ( state: string[] ) => state.length );
`;
expect( getStateArgType( code ) ).toBe( 'string[]' );
} );

it( 'should find types for inner typecasted function with `createSelector`', () => {
const code = `${ docString }
export const getCount = createSelector( (( state: string[] ) => state.length) as any );
`;
expect( getStateArgType( code ) ).toBe( 'string[]' );
} );

it( 'should find types for inner function with `createRegistrySelector`', () => {
const code = `${ docString }
export const getCount = createRegistrySelector( ( select ) => ( state: number ) => state );
`;
expect( getStateArgType( code ) ).toBe( 'number' );
} );
} );
} );