improve typing for partial updates

This commit is contained in:
Ryan Di
2026-04-24 11:10:02 +10:00
parent 75192db663
commit d4836f9e25
+35 -12
View File
@@ -14,6 +14,7 @@ import {
import type { Mutable } from "@excalidraw/common/utility-types"; import type { Mutable } from "@excalidraw/common/utility-types";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawNonSelectionElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
SceneElementsMap, SceneElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
@@ -800,9 +801,19 @@ export class TransactionLedger {
export type TransactionStatus = "active" | "committed" | "canceled"; export type TransactionStatus = "active" | "committed" | "canceled";
/** Per-element partial patch used by tx.updateElements(). */ /** Per-element partial patch used by tx.updateElements(). */
export type TransactionElementUpdate = { type TransactionUpdatableElementType = ExcalidrawNonSelectionElement["type"];
id: ExcalidrawElement["id"]; type TransactionElementOfType<TType extends TransactionUpdatableElementType> =
} & ElementUpdate<OrderedExcalidrawElement>; Extract<ExcalidrawNonSelectionElement, { type: TType }>;
export type TransactionElementUpdate<
TType extends TransactionUpdatableElementType = TransactionUpdatableElementType,
> = TType extends TransactionUpdatableElementType
? {
id: ExcalidrawElement["id"];
type: TType;
updates: ElementUpdate<TransactionElementOfType<TType>>;
}
: never;
/** Final summary returned when a transaction is committed or canceled. */ /** Final summary returned when a transaction is committed or canceled. */
export type TransactionSummary = { export type TransactionSummary = {
@@ -972,21 +983,20 @@ export class Transaction {
* *
* Example: * Example:
* tx.updateElements({ * tx.updateElements({
* elements: [{ id: "a", strokeColor: "#f00" }, { id: "b", x: 10, y: 20 }], * elements: [
* { id: "a", type: "rectangle", updates: { strokeColor: "#f00" } },
* { id: "b", type: "rectangle", updates: { x: 10, y: 20 } },
* ],
* }) * })
*/ */
updateElements<K extends keyof AppState>(data: { updateElements<K extends keyof AppState>(data: {
elements: readonly TransactionElementUpdate[]; elements: readonly TransactionElementUpdate[];
appState?: Pick<AppState, K> | null; appState?: Pick<AppState, K> | null;
}): void { }): void {
const updatesById = new Map< const updatesById = new Map<string, TransactionElementUpdate>();
string,
ElementUpdate<OrderedExcalidrawElement>
>();
for (const update of data.elements) { for (const update of data.elements) {
const { id, ...partialUpdate } = update; updatesById.set(update.id, update);
updatesById.set(id, partialUpdate);
} }
if (updatesById.size === 0) { if (updatesById.size === 0) {
@@ -997,8 +1007,21 @@ export class Transaction {
const nextElements = this.app.scene const nextElements = this.app.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.map((element) => { .map((element) => {
const partialUpdate = updatesById.get(element.id); const update = updatesById.get(element.id);
return partialUpdate ? newElementWith(element, partialUpdate) : element; if (!update) {
return element;
}
if (element.type !== update.type) {
throw new Error(
`Cannot apply tx.updateElements update for "${update.id}": expected "${update.type}", got "${element.type}".`,
);
}
type MatchingElement = TransactionElementOfType<typeof update.type>;
return newElementWith(
element as MatchingElement,
update.updates as ElementUpdate<MatchingElement>,
);
}); });
this.updateScene({ this.updateScene({