-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Entity-aware type signature for getEntityRecord and getEntityRecords #41235
Conversation
packages/core-data/src/selectors.ts
Outdated
|
||
// createSelector isn't properly typed if I don't explicitly import these files – ideally they would | ||
// be merely ambient definitions that TS is aware of. | ||
import type {} from './rememo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's the best way of using this ambient type without TS warnings before enabling "types"
in package.json – these two seem to be connected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth finally submitting a patch to rememo
which adds a types.d.ts
for it, or submitting a new package to the DefinitelyTyped repo. We keep getting back to the point of adding workarounds for it and I think now is as good of a time as ever to make the types real - they aren't that hard to write.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding types.d.ts
would work but I worry it would also create the need to revisit/remove all the jsDoc annotations. I went for a lazy approach of merely adding a @return
annotation. It did the trick in my local testing:
In the meantime, I introduced typed-rememo.ts
to this PR so that we have a path forward in case merging that PR takes a long time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like y'all are on an older version of rememo (v3.0.0). First-party type definitions are available as of v4.0.0, so should hopefully be as simple as upgrading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's used across many different packages, I opened a separate pull request to upgrade them all in one go: #41415
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and merged that PR, thank you so much @aduth! I am AFK this week so I will rebase this one in a few days to confirm it went well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh hi :)
packages/core-data/src/selectors.ts
Outdated
* | ||
* For more details, visit https://github.com/microsoft/TypeScript/issues/23132 | ||
*/ | ||
return filteredItem as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/me looks suspiciously at this comment
going to have to think about this a little. could we get around this by supplying two function type declarations? one in which fields
is a string[]
and which returns Partial<…>
and another in which fields
is missing and which returns EntityRecord<…>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!! I like this idea! I just got it to work in TS playground:
type NumberToNumber = (arg:number) => number;
type StringToString = (arg:string) => string;
const eitherFunction = {} as NumberToNumber & StringToString;
const numberHopefully = eitherFunction(1);
// ^? number
const stringHopefully = eitherFunction('abc');
// ^? string
I'll try to apply this to the selector signature as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dmsnell I ended up getting nowhere with the type intersection operator. The signature overloading via the interface
declaration as follows:
interface GetEntityRecord {
<
R extends EntityRecordOf< 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< K, N >,
query: EntityQuery< C >
): Partial< EntityRecordOf< K, N, C > > | null | undefined;
<
R extends EntityRecordOf< 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< K, N >,
query?: Omit< EntityQuery< C >, '_fields' >
): EntityRecordOf< K, N, C > | null | undefined;
}
const getEntityRecordImplementation: GetEntityRecord = <
R extends EntityRecordOf< 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 >
) => {};
I don't love the proliferation of types here, but it does a good job of distinguishing between the two return types. Trade-offs, trade-offs everywhere 🤷 I've also inlined the getEntityRecordImplementation
1bd4111
✅ One declaration less
❌ as GetEntityRecord
doesn't give us the same type safety as : GetEntityRecord
@dmsnell I went for the getting the dependency graph of
The error is gone when the selector declaration is of form: export const getEntityRecord = createSelector(
< /* generics */ >( state, /* arguments */ ) => {},
() => {}
); But fails with the following forms: export const getEntityRecord = createSelector(
(< /* generics */ >( state, /* arguments */ ) => {}) as GetEntityRecord,
() => {}
);
export const getEntityRecord = createSelector(
getEntityRecordImplementation,
() => {}
); So we may need to revisit docgen once again. |
## What problem does this PR solve? Right now, the `createSelector` function loses the selector type details: ```ts const iReturnNumbers = createSelector( ( stringArg: string ) => 123, () => [] ); const iShouldBeANumber = iReturnNumbers(); // iShouldBeANumber is of type `any` ``` This is a problem in Gutenberg which depends on this package. For example, see the following PR: WordPress/gutenberg#41235 (comment) ## How does this PR propose to solve this problem? Adding the following `@return` type annotation preserves the selector type signature: ``` * @return {S} Memoized selector. ``` The above snippet now works as expected: ```ts const iReturnNumbers = createSelector( ( stringArg: string ) => 123, () => [] ); const iShouldBeANumber = iReturnNumbers(); // iShouldBeANumber is of type `number` ``` ## Test plan This changes just the documentation string, there are no runtime changes to test. Eyeballing the changes and confirming the types are now resolved as in the above examples should suffice. Caveat: this could be considered a breaking change for any projects using `createSelector` in TypeScript. After upgrading rememo to a patched version, the selectors will be of a different type, which could cause type errors. This could be addressed by releasing a new major point release. cc @aduth @dmsnell
} | ||
|
||
type RecordKey = number | string; | ||
type GenericRecordKey = number | string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I retained this for now, but ideally we'll get rid of this type in a follow-up PR that will clean up the types in this file and add any missing signatures.
Concerning Is the problem (a) that |
That’s my understanding, although I did not debug it |
name: N, | ||
key: KeyOf< K, N >, | ||
query: EntityQuery< C > | ||
): Partial< EntityRecordOf< K, N, C > > | null | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if it were a compile time error to refer to a field that you didn't select.
const stuff = core.getEntityRecords( 'root', 'comment', { _fields: [ 'id', 'content' ] } );
console.log( stuff[ 0 ].title ); // shouldn't compile
Maybe this is possible by having the user specify (again...) which fields they are selecting as a type param.
const stuff = core.getEntityRecords< 'id' | 'content' >( 'root', 'comment', { _fields: [ 'id', 'content' ] } );
console.log( stuff[ 0 ].title ); // shouldn't compile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would! I hope to make it happen in one of the next iterations – let's keep this PR at atomic as possible.
ec6058a
to
fc343d4
Compare
Very exciting and mind bending stuff! Thanks for running me through everything at WCEU. I plopped this into the bottom of const userView = getEntityRecord( {} as State, 'root', 'user', 123, {
context: 'view',
} ); |
…g the use of Omit<EntityQuery, '_fields'>
@noisysocks Turns out it was caused by using |
packages/core-data/src/selectors.ts
Outdated
@@ -305,7 +365,12 @@ export const getEntityRecord = createSelector( | |||
]; | |||
} | |||
); | |||
const commentDefault = getEntityRecord( {} as State, 'root', 'comment', 15 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this accidentally, let’s remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM once tests pass! 🙏
What problem does this PR solve?
A subset of #39025
This PR proposes a type signature for the
getEntityRecord
andgetEntityRecords
selectors that supports the following use-cases:And analogous use-cases for
getEntityRecords
.Testing Instructions
cc @dmsnell @sarayourfriend @sirreal