Core Data: Fix return types on generated save/delete entity actions#77162
Core Data: Fix return types on generated save/delete entity actions#77162ObliviousHarmony wants to merge 2 commits intotrunkfrom
Conversation
The dynamically generated per-entity action creators in @wordpress/core-data (saveUser, deletePost, etc.) were typed as `Promise<void>`, but at runtime they delegate to saveEntityRecord/deleteEntityRecord, which resolve with the saved or deleted entity (or a silent-failure sentinel). Consumers were forced into `as unknown` casts to access the real return value. Narrow the mapped-type return values to match runtime reality: - SaveActions resolve to `Entity | undefined` — `undefined` is the silent-failure branch (error swallowed, retrievable via getLastEntitySaveError unless `throwOnError` is set). - DeleteActions resolve to `Entity | false | undefined` — `false` is the silent-failure sentinel (initial value of `deletedRecord`), `undefined` is the early-return path when no entity config is found. Types only; no runtime changes.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: 0 B Total Size: 7.74 MB ℹ️ View Unchanged
|
|
Flaky tests detected in f8dee7a. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24166937984
|
…spatch type
The previous commit on this branch narrowed SaveActions/DeleteActions to
`Promise<Entity | undefined>` / `Promise<Entity | false | undefined>`, which
matches the raw runtime return shape but interacts badly with
`@wordpress/data`'s `PromisifyActionCreator`. That helper has three branches
(thunk, generator, fallthrough) and wraps the result in an outer `Promise<>`.
A function that already returns a `Promise<T>` falls through to the last
branch, yielding `Promise<Promise<T>>` on the dispatched action creator — a
bug that was invisible under the old `Promise<void>` types because
`Promise<Promise<void>>` collapses visually, but surfaced immediately for
downstream consumers (e.g. WooCommerce) removing their `as unknown as` casts.
Model the per-entity save/delete actions as thunks instead, matching what
they actually are at runtime: the wrappers in `index.js` return
`saveEntityRecord` / `deleteEntityRecord`, which are curried thunks of the
shape `(…args) => ({dispatch, select, resolveSelect}) => Promise<…>`. This
lets `PromisifyActionCreator`'s thunk branch flatten the dispatched shape to
a single `Promise<Entity | undefined>` / `Promise<Entity | false | undefined>`.
No runtime changes. All 764 core-data unit tests pass and the full repo
typecheck (`tsc --build`) is clean.
What?
Narrows the return types of the dynamically generated per-entity action creators in
@wordpress/core-data(saveUser,deletePost,saveTerm, etc.) to match the runtime reality. Previously they were typed asPromise<void>, even though at runtime they delegate tosaveEntityRecord/deleteEntityRecordand resolve with the saved/deleted entity.Why?
Types only — no runtime behavior changes. This corrects a longstanding mismatch between the declared types and the actual resolved values, which forced downstream consumers (Gutenberg itself, WooCommerce, and any plugin using
dispatch( coreStore ).save<Entity>( ... )) intoas unknowncasts to access the real return value.A single mapped-type fix corrects the return type of 19 save and 19 delete actions:
Comment,GlobalStyles,Media,Menu,MenuItem,MenuLocation,Plugin,PostType,Revision,Sidebar,Site,Status,Taxonomy,Term,Theme,UnstableBase,User,Widget,WidgetType.How?
Updated the
SaveActionsandDeleteActionsmapped types inpackages/core-data/src/dynamic-entities.tsto reuse theKey extends \save${infer E}`/`delete${infer E}`extraction that's already present in the file (seeSingularGettersjust above), and apply it in the return position. Kept the change as a mapped type rather than expanding it into hand-written entries, so it stays in sync withWPEntityTypes` automatically.Runtime verification:
saveEntityRecordinpackages/core-data/src/actions.jsreturnsupdatedRecord(the saved entity) on success. On silent failure (error swallowed, surfaced viagetLastEntitySaveErrorunlessthrowOnError: true), it returnsundefined. New return type:Promise< Entity | undefined >.deleteEntityRecordreturnsdeletedRecord, which is initialized tofalseand overwritten with the REST DELETE response on success. Thefalsesentinel survives on silent failure, and the function returnsundefinedvia the early exit when no entity config is found. New return type:Promise< Entity | false | undefined >.Migration note for consumers: code that previously did
(await dispatch( coreStore ).saveFoo( x )) as unknown as Foocan drop the cast, but the new return type makes the silent-failure branches visible. Either handleundefined(save) /false | undefined(delete), or pass{ throwOnError: true }.Testing Instructions
npx tsc --build packages/core-data— should complete with no new errors.npm run test:unit -- packages/core-data— all 764 core-data tests pass.packages/core-data/build-types/index.d.tsand confirm the generatedsave<Entity>/delete<Entity>entries (e.g.saveUser,deleteMedia) resolve toPromise<Entity | undefined>andPromise<Entity | false | undefined>respectively, rather thanPromise<void>.Testing Instructions for Keyboard
N/A — types-only change with no UI impact.
Screenshots or screencast
N/A — types-only change.
Use of AI Tools
Developed with AI assistance (Claude Code). The runtime of
saveEntityRecordanddeleteEntityRecordwas verified by readingpackages/core-data/src/actions.jsdirectly, the mapped-type change was reviewed against the existingSingularGetterspattern in the same file, and the test suite was run locally. I have reviewed the change and take responsibility for it.