Skip to content

Commit

Permalink
TypeScript definitions for core-data entity records (#38666)
Browse files Browse the repository at this point in the history
* Add types for core data entity records

* Use string enums instead of numeric ones

* Put each type in a separate file

* Rond 2 of autogenerating types

* Extract enum types

* Extract common interfaces

* Add type definitions for posts and template parts

* Add Raw Data typings

* Extract AvatarUrls to common.js

* Try context-based entity types

* Experimenting with different ways of contextualizing data types

* Remove EntityInContext – it isn't really needed

* Rename RawDataIsString to RawDataOverride

* Use Entity and EntityWithEdits without distinguishing between different contexts

* Make all the fields contextual

* Refactir WithEdits to EditedRecord

* Rename "Edited" to "Updatable"

* Flatten Nevers and WithoutNevers to OmitNevers

* Remove dedicated updatable fields in favor of an Updateble type wrapper

* Remove extra wrappers around types

* Add missing definitions to NavMenu type

* Use RawField in Page and Post types

* Export UpdatableRecord type

* Rename RawField to RenderedText

* Export updatable types

* Rename UpdatableRecord to Updatable

* Remove atomic updatable types

* Adjust Comment status and User locale modeling

* Remove the NestedWidget name, declare the type inline in Sidebar

* Make User.password optional

* Introduce StringWhenUpdatable type to model WP templates

* Document the Updatable type

* Document the RenderedText using @dmsnell's proposal

* Document types in common.ts

* Flatten the content prop of WpTemplate and WpTemplatePart into a string through a condition inside the Updatable type wrapper

* Type the remaining optional fields more strictly

* Wrap Type with OmitNevers

* Wrap the type user with OmitNevers

* Use consistent kebab case in type files

* Wrap comment with OmitNevers

* Add the missing OmitNevers to types with contextual fields

* Add README.md

* Rename common.ts to helpers.ts

* Model WpTemplate.content and WpTemplatePart.content as RenderableText

* Link to the REST API docs when explaining contexts

* Explain the ContextualFields without meandering on the implementation details of `never` fields

* Use the correct capitalization of the word javascript

* Rename CommentStatus to CommentingStatus in context of comments container

* Use Post as an example illustrating the usage of ContextualField

* Focus on the goal of the ContextualField in its documentation

* Use a cleaner explanation of the Updatable type wrapper

* Explain why the types do not provide full type safety

* Remove the TODO comments from the README and a section about extendability

* Add extensible type prefix

* a -> an

* Use export type and import type declarations

* Note that Comment.id is still a field even after the Comment type has been extended

* Use CommentingStatus and PingStatus in the Attachment type

* Use a correct snippet of code to illustrate interface extending in README.md

* Use namespaced base types for extenders

* Document the BaseTypes namespace

* Lint

* Lint the docstring in base-types.ts

* Rename the BaseTypes namespace to WPBaseTypes

* Add more commentary to the Extending section

* Add a comma

* Clarify the warning about type safety

* Restore the first sentence of the warning

* Link to the types readme in core-data readme

* Do not publish the data types

* Rename WPBaseTypes to CoreBaseEntityTypes

* Rename CoreBaseEntityTypes to BaseEntityTypes

* Fix a typo (extends -> extend)
  • Loading branch information
adamziel authored Feb 22, 2022
1 parent 8c3ca76 commit 87a7fbe
Show file tree
Hide file tree
Showing 25 changed files with 2,231 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/core-data/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Core Data

Core Data is a [data module](https://github.com/WordPress/gutenberg/tree/HEAD/packages/data/README.md) intended to simplify access to and manipulation of core WordPress entities. It registers its own store and provides a number of selectors which resolve data from the WordPress REST API automatically, along with dispatching action creators to manipulate data.
Core Data is a [data module](https://github.com/WordPress/gutenberg/tree/HEAD/packages/data/README.md) intended to simplify access to and manipulation of core WordPress entities. It registers its own store and provides a number of selectors which resolve data from the WordPress REST API automatically, along with dispatching action creators to manipulate data. Core data is shipped with [`TypeScript definitions for WordPress data types`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/core-data/src/types/README.md).

Used in combination with features of the data module such as [`subscribe`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/data/README.md#subscribe-function) or [higher-order components](https://github.com/WordPress/gutenberg/tree/HEAD/packages/data/README.md#higher-order-components), it enables a developer to easily add data into the logic and display of their plugin.

Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export { default as __experimentalUseEntityRecord } from './hooks/use-entity-rec
export { default as __experimentalUseEntityRecords } from './hooks/use-entity-records';
export * from './entity-provider';
export * from './fetch';
export * from './types';
193 changes: 193 additions & 0 deletions packages/core-data/src/types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Entity Records Types

## Overview

The types in this directory are designed to support the following use-cases:

* Provide type-hinting and documentation for entity records fetched in the various REST API contexts.
* Type-check the values we use to *edit* entity records, the values that are sent back to the server as updates.

**Warning:** The types model the expected API responses which is **not** the same as having a full type safety for the API-related operations. The API responses are type-cast to these definitions and therefore may not match those expectations; for example, a plugin could modify the response, or the API endpoint could have a nuanced implementation in which strings are sometimes used instead of numbers.

### Context-aware type checks for entity records

WordPress REST API returns different responses based on the `context` query parameter, which typically is one of `view`, `edit`, or `embed`. See the [REST API documentation](https://developer.wordpress.org/rest-api/) to learn more.

For example, requesting `/wp/v2/posts/1?context=view` yields:

```js
{
"content": {
"protected": false,
"rendered": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n"
},
"title": {
"rendered": "Hello world!"
}
// other fields
}
```

While requesting `/wp/v2/posts/1?context=edit`, yields:

```js
{
"content": {
"block_version": 1,
"protected": false,
"raw": "<!-- wp:paragraph -->\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n<!-- /wp:paragraph -->",
"rendered": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n"
},
"title": {
"raw": "Hello world!",
"rendered": "Hello world!"
}
// other fields
}
```

And, finally, requesting `/wp/v2/posts/1?context=embed` yields:

```js
{
// Note content is missing
"title": {
"rendered": "Hello world!"
}
// other fields
}
```

These contexts are supported by the core-data resolvers like `getEntityRecord()` and `getEntityRecords()` to retrieve the appropriate "flavor" of the data.

The types describing different entity records must thus be aware of the relevant API context. This is implemented using the `Context` type parameter. For example, the implementation of the `Post` type resembles the following snippet:

```ts
interface Post<C extends Context> {
/**
* A named status for the post.
*/
status: ContextualField< PostStatus, 'view' | 'edit', C >;

// ... other fields ...
}
```

The `status` field is a `PostStatus` when the requesting context is `view` or `edit`, but if requested with an `embed` context the field won't appear on the `Post` object at all.

### Static type checks for *edited* entity records, where certain fields become strings instead of objects.

When the `post` is retrieved using `getEntityRecord`, its `content` field is an object:

```js
const post = wp.data.select('core').getEntityRecord( 'postType', 'post', 1, { context: 'view' } )
// `post.content` is an object with two fields: protected and rendered
```

The block markup stored in `content` can only be rendered on the server so the REST API exposes both the raw markup and the rendered version. For example, `content.rendered` could used as a visual preview, and `content.raw` could be used to populate the code editor.

When updating that field from the JavaScript code, however, all we can set is the raw value that the server will eventually render. The API expects us to send a much simpler `string` form which is the raw form that needs to be stored in the database.

The types reflect this through the `Updatable<EntityRecord>` wrapper:

```ts
interface Post< C extends Context > {
title: {
raw: string;
rendered: string;
}
}

const post : Post< 'edit' > = ...
// post.title is an object with properties `raw` and `rendered`

const post : Updatable<Post< 'edit' >> = ...
// post.title is a string
```

The `getEditedEntityRecord` selector returns the Updatable version of the entity records:

```js
const post = wp.data.select('core').getEditedEntityRecord( 'postType', 'post', 1 );
// `post.content` is a string
```

## Helpers

### Context

The REST API context parameter.

### ContextualField

`ContextualField` makes the field available only in the specified given contexts, and ensure the field is absent from the object when in a different context.

Example:

```ts
interface Post< C extends Context > {
modified: ContextualField< string, 'edit' | 'view', C >;
password: ContextualField< string, 'edit', C >;
}

const post: Post<'edit'> =
// post.modified exists as a string
// post.password exists as a string

const post: Post<'view'> =
// post.modified still exists as a string
// post.password is missing, undefined, because we're not in the `edit` context.
```

### OmitNevers

Removes all the properties of type never, even the deeply nested ones.

```ts
type MyType = {
foo: string;
bar: never;
nested: {
foo: string;
bar: never;
}
}
const x = {} as OmitNevers<MyType>;
// x is of type { foo: string; nested: { foo: string; }}
// The `never` properties were removed entirely
```

### Updatable

Updatable<EntityRecord> is a type describing Edited Entity Records. They are like
regular Entity Records, but they have all the local edits applied on top of the REST API data.

This turns certain field from an object into a string.

Entities like Post have fields that only be rendered on the server, like title, excerpt,
and content. The REST API exposes both the raw markup and the rendered version of those fields.
For example, in the block editor, content.rendered could used as a visual preview, and
content.raw could be used to populate the code editor.

When updating these rendered fields, JavaScript is not be able to properly render arbitrary block
markup. Therefore, it stores only the raw markup without the rendered part. And since that's a string,
the entire field becomes a string.

```ts
type Post< C extends Context > {
title: RenderedText< C >;
}
const post = {} as Post;
// post.title is an object with raw and rendered properties

const updatablePost = {} as Updatable< Post >;
// updatablePost.title is a string
```

### RenderedText

A string that the server renders which often involves modifications from the raw source string.

For example, block HTML with the comment delimiters exists in `post_content` but those comments are stripped out when rendering to a page view. Similarly, plugins might modify content or replace shortcodes.
146 changes: 146 additions & 0 deletions packages/core-data/src/types/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Internal dependencies
*/
import {
Context,
ContextualField,
MediaType,
PostStatus,
RenderedText,
OmitNevers,
CommentingStatus,
PingStatus,
} from './helpers';

import { BaseEntityTypes as _BaseEntityTypes } from './base-entity-types';

declare module './base-entity-types' {
export namespace BaseEntityTypes {
export interface Attachment< C extends Context > {
/**
* The date the post was published, in the site's timezone.
*/
date: string | null;
/**
* The date the post was published, as GMT.
*/
date_gmt: ContextualField< string | null, 'view' | 'edit', C >;
/**
* The globally unique identifier for the post.
*/
guid: ContextualField< RenderedText< C >, 'view' | 'edit', C >;
/**
* Unique identifier for the post.
*/
id: number;
/**
* URL to the post.
*/
link: string;
/**
* The date the post was last modified, in the site's timezone.
*/
modified: ContextualField< string, 'view' | 'edit', C >;
/**
* The date the post was last modified, as GMT.
*/
modified_gmt: ContextualField< string, 'view' | 'edit', C >;
/**
* An alphanumeric identifier for the post unique to its type.
*/
slug: string;
/**
* A named status for the post.
*/
status: ContextualField< PostStatus, 'view' | 'edit', C >;
/**
* Type of post.
*/
type: string;
/**
* Permalink template for the post.
*/
permalink_template: ContextualField< string, 'edit', C >;
/**
* Slug automatically generated from the post title.
*/
generated_slug: ContextualField< string, 'edit', C >;
/**
* The title for the post.
*/
title: RenderedText< C >;
/**
* The ID for the author of the post.
*/
author: number;
/**
* Whether or not comments are open on the post.
*/
comment_status: ContextualField<
CommentingStatus,
'view' | 'edit',
C
>;
/**
* Whether or not the post can be pinged.
*/
ping_status: ContextualField< PingStatus, 'view' | 'edit', C >;
/**
* Meta fields.
*/
meta: ContextualField<
Record< string, string >,
'view' | 'edit',
C
>;
/**
* The theme file to use to display the post.
*/
template: ContextualField< string, 'view' | 'edit', C >;
/**
* Alternative text to display when attachment is not displayed.
*/
alt_text: string;
/**
* The attachment caption.
*/
caption: ContextualField< string, 'edit', C >;
/**
* The attachment description.
*/
description: ContextualField<
RenderedText< C >,
'view' | 'edit',
C
>;
/**
* Attachment type.
*/
media_type: MediaType;
/**
* The attachment MIME type.
*/
mime_type: string;
/**
* Details about the media file, specific to its type.
*/
media_details: Record< string, string >;
/**
* The ID for the associated post of the attachment.
*/
post: ContextualField< number, 'view' | 'edit', C >;
/**
* URL to the original attachment file.
*/
source_url: string;
/**
* List of the missing image sizes of the attachment.
*/
missing_image_sizes: ContextualField< string[], 'edit', C >;
}
}
}

export type Attachment< C extends Context > = OmitNevers<
_BaseEntityTypes.Attachment< C >
>;
36 changes: 36 additions & 0 deletions packages/core-data/src/types/base-entity-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* This module exists solely to make the BaseEntityTypes namespace extensible
* with declaration merging:
*
* ```ts
* declare module './base-entity-types' {
* export namespace BaseEntityTypes {
* export interface Comment< C extends Context > {
* id: number;
* // ...
* }
* }
* }
* ```
*
* The huge upside is that consumers of @wordpress/core-data may extend the
* exported data types using interface merging as follows:
*
* ```ts
* import type { Context } from '@wordpress/core-data';
* declare module '@wordpress/core-data' {
* export namespace BaseEntityTypes {
* export interface Comment< C extends Context > {
* numberOfViews: number;
* }
* }
* }
*
* import type { Comment } from '@wordpress/core-data';
* const c : Comment< 'view' > = ...;
*
* // c.numberOfViews is a number
* // c.id is still present
* ```
*/
export namespace BaseEntityTypes {}
Loading

0 comments on commit 87a7fbe

Please sign in to comment.