-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Core Data: TypeScript definitions for entity records. #38666
Conversation
Thanks for posting this @adamziel. Here are a few quick thoughts I had while skimming:
Can you clarify what you mean here by edit, view, and embed in context of things like attachment or theme objects? If they are generalizable it'd likely work out well to have the other direction: type EditableEntity<Entity> = Entity;
type ViewableEntity<Entity> = Partial<Entity>; but I don't feel like I understand exactly what you are wanting to do and what needs we have. as a reminder, there is no real typing without a purpose for the typing, so as you mention here, what we want and need in different contexts is going to lead the design of the types.
Are you talking about the fact that we might store a string in the app but the API response has a different version of the data? If that's the case we may find that we have a different type for the API response and the internal data. That's a good thing though! By the way we can handle these cases through type utilities if the case is general enough and warranted.
it's worth a look but my experience tells me the duplicate code would be worth it. they will likely diverge in their evolution and be used in different ways or places. if they have enough overlap we might discover that there's only a single type, but likely two is fine.
👍 to this. I generally try to avoid type unions inside of record definitions // not this
type Alert = {
criticality: 'high' | 'low';
ttl: number;
}
// but this
type Criticality = 'high' | 'low';
type Alert = {
criticality: Criticality;
ttl: number;
}
API response types are inherently a lie. we can't rely on them so they are more or less a heuristic tool for developers to discover properties they expect. they are going to represent a safety hole in the system though if we cast an API response to these types. the only way to provide a durable and safe type is to only create it after parsing an API response where a failure to adhere to the expectations produces a parse failure instead of an invalidly-typed object. that's not likely to be useful here. all that is to say as far as maintenance goes I'd bet we're fine leaving these and letting people fix or change them as WordPress changes. remember that plugins might alter these API responses as well so really we can't say anything for sure about them. given that uncertainty they can be helpful for what they are: reasonable guides.
Same as the last thing I mentioned. We can type everything What is the primary goal of adding these types? |
I only wanted to link the related tracking issue #18838. |
Good call @dmsnell! I just updated this PR
I don't have laser focused requirements. The ultimate goal is to enjoy the usual benefits of static analysis across Gutenberg, and That being said, it would already be quite useful with useSelect to help with common mistakes like: function PostTitle({ postId }) {
// TypeScript understands that post is a Post or undefined
const post = useSelect( select => select( coreStore ).getEntityRecord(
'postType',
'post',
postId,
{ context: 'view' }
), [ postId ]);
return (
<div>
{/* TS warns me post may still be undefined */}
{post.title.rendered}
{/* TS warns me I have the property name wrong */}
{post.name}
{/* Brownie points: TS warns me this property is not available in the view context */}
{post.title.raw}
</div>
);
}
WordPress REST API returns different subsets of field depending on the
We see that {
"date": {
"description": "The date the post was published, in the site's timezone.",
"type": [ "string", "null" ],
"format": "date-time",
"context": [ "view", "edit", "embed" ]
}
} This field will be exposed only for requests with Now, here's a different field: {
"description": {
"description": "The attachment description.",
"type": "object",
"context": [
"view",
"edit"
]
} It won't be returned by the API if I send a request with
Good spot, it looks weird to me, too! Based on the underlying JSON schema I quoted earlier, it may be either a string or a null, but it shows up in all the contexts supported by the media endpoint so we can just say
It is awkward indeed.
Ah, I meant a quirk of how core-data treats certain properties. For example, posts retrieved using
👍
Just to confirm I understand - you are saying that casting the actual data type received from the API to the one declared by the API represents a safety hole, correct?
👍 |
Spontaneous idea: To handle contexts, we could define "full" types with all the properties, and then derive context-based types using `Pick: interface Attachment {
// ... lots of fields
}
// Let's find a better name :-)
type AttachmentInViewContext = Pick<Attachment, 'date' | 'guid' | ...>; That doesn't solve the varying shape of "raw" fields like interface RawFieldInViewContext {
rendered: string;
}
interface RawFieldInEditContext {
raw: string;
rendered: string;
}
interface Attachment {
// ... lots of fields
}
interface AttachmentInViewContext extends Pick<Attachment, 'date' | ...> {
guid: RawFieldInViewContext;
}
interface AttachmentInEditContext extends Pick<Attachment, 'date' | ...> {
guid: RawFieldInEditContext;
} It's getting pretty complex, though. Maybe the returns are diminishing at this point? We could move forward with the optional fields and iterate if needed. |
I just found these Typescript types from @johnbillion: https://github.com/johnbillion/wp-json-schemas/blob/trunk/packages/wp-types/index.ts |
Related issue for wp-types: johnbillion/wp-json-schemas#40 |
I added some types for raw data fields: type Post< RawObject > // title is an object like {raw: "", rendered: ""}
type Post< RawString > // title is a string I'm still noodling on a different naming scheme like |
I just had this idea for entity types with context-specific fields: interface BaseAttachment {
// ... lots of fields ...
}
type ViewContext = 'ViewContext';
type EditContext = 'EditContext';
type EmbedContext = 'EmbedContext';
type EntityContext = ViewContext | EditContext | EmbedContext;
type ViewProperties = 'date' | 'date_gmt';
type EditProperties = 'date' | 'id';
export type Attachment< Context extends EntityContext > = Pick<
BaseAttachment,
Context extends ViewContext ? ViewProperties : EditProperties
>;
// In another file:
const photo : Attachment< EditContext > = getEntityRecord( 'root', 'media', 15 ); It would require quite a bit of refactoring, though, so I'll wait for feedback before acting on this. |
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.
Attachment< EditContext >
This feels inverted to me. I don't think we'll really want to inject the context into the type and then have the type decide what it is based on that context.
Your code snippet looks fine but maybe we could consider instead of Attachment<Context>
and BaseAttachment
something like Attachment
and AttachmentInContent<EditContext>
Really I'd rather personally be dealing with EditContext<Attachment>
but I can see where it would be hard to create the typings for that, would need to couple all the different types together, unless we made those inside each module.
// attachment.ts
export interface Attachment … {}
export type InContext<Context extends AttachmentContext> =
Context extends EditContext ? Pick<Attachment, EditFields> :
Context extends ViewContext ? Pick<Attachyment, ViewFields> :
never;
import * as Attachment from '…/attachment';
import { EditContext } from '…/entity-contexts…';
const attachment: Attachment.InContext<EditContext> = …
Just thoughts. I wish we could have it all ways and I'm not sure what's best here, just that something about seeing Attachment<EditContext>
worries me about muddying our interfaces.
First of all, I think this will be a really big scale and can be hard to tackle, so thank you for starting this! I agree that the optional field is awkward ( As for keeping the types in sync with the API, I have no knowledge of how the types are generated and maintained on the PHP side, but I'd imagine a workflow to also type check the PHP response (either in runtime tests or static type check) so that we can make sure the schema is always valid. Then, we can repeat what @adamziel have done here, generate the typescript definition files via a script automatically whenever those schema change. Not sure how doable it is though, and might be a good candidate for another PR rather than this one. One challenge here might be that the code of the REST API is in both repo (gutenberg and core), not sure what to do with it (another benefit to having a monorepo 😆 ?).
This might also be the simplest method I can think of too. We can create the Note that we probably won't need to cast it ourselves anyway, |
All the checks are green – this could be 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.
Fantastic, exciting, wonderful! I don't have enough positive adjectives for how exciting it is that this is working 🎉
I don't think we should publish these types though, unless my understanding of how this package is used is incorrect. As long as I understand things correctly, to be able to publish these, we'll need to at minimum be able to implement the additional interfaces to the @wordpress/data
types like happen in the DefinitelyTyped types here:
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.
Found a typo but I'm really happy with all these. Well done 👏
* const c : Comment< 'view' > = ...; | ||
* | ||
* // c.numberOfViews is a number | ||
* // c.id is still present |
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 love this all so much ❤️
Impressive work everyone 👏🏻 |
When we introduced the types for the core entities in #38666 we used a directory named `types`. Unfortunately this meant that we can't create the customary file `src/types.ts` because of the name clash in module resolution. In this patch we're renaming the directory so that we can separately import `entity-types(/index.ts)` and also `types(.ts)` without the naming conflict.
When we introduced the types for the core entities in #38666 we used a directory named `types`. Unfortunately this meant that we can't create the customary file `src/types.ts` because of the name clash in module resolution. In this patch we're renaming the directory so that we can separately import `entity-types(/index.ts)` and also `types(.ts)` without the naming conflict.
capabilities: ContextualField< | ||
Record< string, string >, | ||
'edit', | ||
C | ||
>; | ||
/** | ||
* Any extra capabilities assigned to the user. | ||
*/ | ||
extra_capabilities: ContextualField< | ||
Record< string, string >, | ||
'edit', | ||
C | ||
>; |
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.
These capabilities are actually Record< string, boolean >
instead of Record< string, string >
and these come from here.
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.
Created #68045 to fix it.
Description
This PR adds type definitions for default core data entity records:
The fields that are only present in certain contexts are defined as in the JSON schema, for example:
The locally edited versions of the records are wrapped with
UpdatableRecord
:This should allow us to build towards the following goals:
getEntityRecord
,getEntityRecords
,select()
,resolveSelect()
, anduseSelect()
title.raw
andtitle.rendered
vs just the.title
.Post
withcontext: 'view'
, TS will know there's no such field aspost.content.raw
, but if you fetch the samePost
withcontext: edit
,post.content.raw
will be a string.The types are not published for now. When we do publish them, we'll have to restore the following README section on extending: https://gist.github.com/adamziel/ec41157c10ac11286609a635adb35a0b
Follow-up tasks:
cc @kevin940726 @dmsnell @jsnajdr @ntsekouras @mcsf @draganescu @noisysocks @talldan @tellthemachines @mkaz @gziolo