docs/plans/2026-05-16-slate-v2-boolean-mark-key-type-helper-ralplan.md
Yes. The example-local BooleanTextKey<T> mapped type is bad DX. It makes the
example teach TypeScript plumbing instead of Slate.
Accepted target: add Slate-owned type helpers for boolean mark keys and boolean mark objects, then make examples consume those helpers.
This is a type-surface and example-DX plan only. It does not change mark runtime semantics, transform behavior, React rendering, or collaboration behavior.
type CustomTextKey = BooleanMarkKeysOf<CustomText> without copying mapped
conditional types into examples.slate type exports, generic type contracts, examples that use
CustomTextKey, PR/reference wording, and issue accounting for the related
TypeScript-formatting DX issue.MarkKeysOf semantic change, no backward
compatibility alias.Live source proves the bad shape exists now:
type BooleanTextKey<T> = {
[K in keyof T]: Exclude<T[K], undefined> extends boolean ? K : never;
}[keyof T];
export type CustomTextKey = Extract<BooleanTextKey<CustomText>, string>;
Source: .tmp/slate-v2/site/examples/ts/custom-types.d.ts.
The copied type feeds public example helpers:
type ActiveMarks = Partial<Pick<CustomText, CustomTextKey>>;
export const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
editor.update((tx) => {
tx.marks.toggle(format);
});
};
Source: .tmp/slate-v2/site/examples/ts/mark-utils.ts.
Current core already owns adjacent generic mark helpers:
export type MarksOf<N> = Simplify<UnionToIntersection<TextProps<TextOf<N>>>>;
export type MarksIn<V extends readonly unknown[]> = MarksOf<V[number]>;
export type MarkKeysOf<N> = {} extends MarksOf<N> ? unknown : keyof MarksOf<N>;
Source: .tmp/slate-v2/packages/slate/src/interfaces/text.ts.
Current tests intentionally preserve the MarkKeysOf fallback:
type _OptionalMarkKeysFollowPlateFallback = Assert<
Equal<MarkKeysOf<ParagraphElement>, unknown>
>;
Source: .tmp/slate-v2/packages/slate/test/generic-value-contract.ts.
Principles:
slate only when they name Slate data, not generic TS
utility trivia.Top drivers:
MarkKeysOf is too broad and intentionally falls back to unknown for
optional marks.MarksOf<N>.Options:
| Option | Verdict | Reason |
|---|---|---|
Keep BooleanTextKey in examples | reject | This leaks advanced TS into starter code and duplicates a Slate concept. |
Change MarkKeysOf to return optional mark keys | reject | It silently changes an existing contract and broadens keys beyond boolean marks. |
Export generic BooleanKeys<T> | reject | Too general for Slate core; it looks like a utility library. |
Add BooleanMarkKeysOf<N> only | revise | Fixes key typing but leaves ActiveMarks = Partial<Pick<...>> noise. |
Add BooleanMarkKeysOf<N> and BooleanMarksOf<N> | choose | Small, Slate-named, type-only, and enough to clean examples without runtime API. |
Chosen option:
export type BooleanMarkKeysOf<N> = Extract<
{
[K in keyof MarksOf<N>]-?: Exclude<MarksOf<N>[K], undefined> extends boolean
? K
: never;
}[keyof MarksOf<N>],
string
>;
export type BooleanMarksOf<N> = Partial<Pick<MarksOf<N>, BooleanMarkKeysOf<N>>>;
Consequences:
BooleanMarkKeysOf<CustomText> returns boolean mark keys such as bold,
italic, code, and underline.fontSize?: string is excluded.MarkKeysOf<ParagraphElement> can keep returning unknown.Follow-ups:
slate type surface.BooleanTextKey.Add to .tmp/slate-v2/packages/slate/src/interfaces/text.ts:
export type BooleanMarkKeysOf<N> = Extract<
{
[K in keyof MarksOf<N>]-?: Exclude<MarksOf<N>[K], undefined> extends boolean
? K
: never;
}[keyof MarksOf<N>],
string
>;
export type BooleanMarksOf<N> = Partial<Pick<MarksOf<N>, BooleanMarkKeysOf<N>>>;
Export through .tmp/slate-v2/packages/slate/src/index.ts next to MarksOf,
MarksIn, and MarkKeysOf.
Example target:
import type { BooleanMarkKeysOf } from "slate";
export type CustomTextKey = BooleanMarkKeysOf<CustomText>;
Mark helper target:
import type { BooleanMarksOf } from "slate";
type ActiveMarks = BooleanMarksOf<CustomText>;
Do not export a generic BooleanKeys<T> unless another Slate-owned type surface
needs it later.
No runtime change.
The helper derives from MarksOf<N>, so it follows the current value-first type
model and does not introduce a schema registry, mark registry, plugin registry,
or toolbar abstraction.
Examples should show:
CustomTextKey as a Slate-derived mark key union.CustomTextKey.Affected live examples:
.tmp/slate-v2/site/examples/ts/custom-types.d.ts.tmp/slate-v2/site/examples/ts/mark-utils.ts.tmp/slate-v2/site/examples/ts/richtext.tsx.tmp/slate-v2/site/examples/ts/hovering-toolbar.tsx.tmp/slate-v2/site/examples/ts/iframe.tsxPlate/plugin authors can map this directly to their own text type. The helper does not require current Plate API compatibility and does not constrain Plate's plugin-level mark config.
slate-yjs/collab is unaffected. Mark values and operations stay unchanged; only TypeScript authoring gets a narrower key/object helper.
ClawSweeper related-issue pass: ledger-first, no live GitHub needed.
Touched issue:
| Issue | Cluster | Claim | Why | Proof route | V2 sync ledger | PR line |
|---|---|---|---|---|---|---|
| #5075 | example-typescript-ergonomics | Improves after execution | The issue is exactly formatting-key TypeScript ergonomics. This plan removes the copied example type stunt and gives examples a Slate-owned helper. Do not mark Fixes without replaying the exact original issue shape. | type contract plus example typecheck | currently not-claimed; execution may move to improves with proof | related matrix only unless exact closure is proven |
Related but not fixed:
PR reference sync: required because accepted API shape and examples change.
| System | Source | Mechanism | Avoids | Steal | Reject | Slate target | Verdict |
|---|---|---|---|---|---|---|---|
| Current Slate v2 | .tmp/slate-v2/packages/slate/src/interfaces/text.ts | MarksOf<N> derives marks from TextOf<N> and strips text. | Ad hoc Omit<T, 'text'> copies in every example. | Reuse MarksOf<N> as the base. | Changing MarkKeysOf fallback. | Add boolean-specific helpers beside MarksOf. | agree |
| Current Slate v2 tests | .tmp/slate-v2/packages/slate/test/generic-value-contract.ts | MarkKeysOf intentionally returns unknown for optional marks. | Breaking existing type expectations. | Add new contracts for boolean mark keys. | Mutating old helper semantics. | Keep old tests green, add new helper tests. | agree |
| Prior generics plan | docs/plans/2026-04-26-slate-v2-plate-generics-type-system-plan.md | Value-first helpers derive marks from text unions. | Declaration-merging and any mark objects. | Keep helper derivation value-first. | A generic schema object as primary typing model. | Type-only helper derived from current value model. | agree |
| Issue corpus | docs/slate-issues/requirements-from-issues.md | Public API/type surface needs helpers matching actual runtime guarantees. | Growing core because docs are weak. | Small type helper where runtime guarantee is real. | Product toolbar helper in raw core. | Type-only public helper, no runtime abstraction. | partial |
No Lexical, ProseMirror, or Tiptap evidence is needed for this narrow type helper. This is not a runtime architecture decision.
| Lens | Applicability | Finding | Plan delta |
|---|---|---|---|
| Vercel React | skipped | No React render/subscription/effect change. | None. |
| performance-oracle | applied | Type-only exports have zero runtime cost; helper should not add runtime mark registry. | Keep helper type-only. |
| performance | skipped | No production perf claim or benchmark surface. | None. |
| tdd | applied | Public type helper needs type-contract proof, not implementation-detail tests. | Add compile contract before example migration. |
| shadcn | skipped | No UI component surface changes. | None. |
| react-useeffect | skipped | No effects or external synchronization. | None. |
Triggered because this changes public type API.
Pre-mortem:
fontSize; test should
reject it.MarkKeysOf optional fallback by refactoring shared types; keep
existing contract tests.keyof EditorMarksOf and lets
arbitrary string-valued marks into boolean toggles.Expanded proof plan:
BooleanMarkKeysOf<CustomText> equals expected boolean keys.BooleanMarksOf<CustomText> excludes fontSize.MarkKeysOf<ParagraphElement> remains unknown.BooleanTextKey.Blast radius:
packages/slate/src/interfaces/text.tspackages/slate/src/index.tspackages/slate/test/generic-value-contract.tssite/examples/ts/custom-types.d.tssite/examples/ts/mark-utils.tsCustomTextKeyRollback answer:
| Change | Likely objection | Steelman antithesis | Tradeoff tension | Answer | Rejected alternative | Proof | Verdict |
|---|---|---|---|---|---|---|---|
Add BooleanMarkKeysOf<N> and BooleanMarksOf<N> | "This is too niche for core; examples can keep their own type." | Slate core should stay minimal and avoid every docs convenience. | Two new public type names are API surface. | The helper names a Slate concept: boolean marks. The current example has to reimplement it because MarkKeysOf intentionally does not solve optional boolean marks. Type-only, no runtime cost, no product API. | Generic BooleanKeys<T> and changing MarkKeysOf. | Generic type contracts plus example typecheck. | keep |
BooleanTextKey.MarkKeysOf.BooleanKeys<T> as public API.toggleBooleanMark, toolbar helpers, hotkey helpers, or
Plate-style mark config to raw Slate.| Risk | Proof |
|---|---|
| Boolean helper includes string-valued marks | type contract rejects fontSize |
Optional boolean marks collapse to unknown | type contract expects concrete union |
Existing MarkKeysOf contract changes | existing generic-value-contract.ts rows stay green |
| Examples still carry copied helper | rg "BooleanTextKey" site/examples/ts returns none |
| Example helpers lose active-mark precision | BooleanMarksOf<CustomText> used in mark-utils.ts |
No browser proof required for this planning slice or the helper itself. The later implementation should typecheck examples. Existing browser rows for formatting examples remain owned by their runtime plans because runtime behavior does not change here.
BooleanMarkKeysOf / BooleanMarksOf expectations
to .tmp/slate-v2/packages/slate/test/generic-value-contract.ts..tmp/slate-v2/packages/slate/src/interfaces/text.ts and public index export.BooleanTextKey in
.tmp/slate-v2/site/examples/ts/custom-types.d.ts; use
BooleanMarksOf<CustomText> in mark-utils.ts.CustomTextKey usage without local generic gymnastics..tmp/slate-v2: bunx tsc --project packages/slate/test/tsconfig.generic-types.json --noEmit --pretty false.tmp/slate-v2: bun --filter slate typecheck.tmp/slate-v2: bun typecheck:site or the repo's current site/example
typecheck command if different.tmp/slate-v2: rg "BooleanTextKey" site/examples/tsplate-2: pnpm lint:fixplate-2: node tooling/scripts/completion-check.mjs| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.98 | no runtime/React change |
| Slate-close unopinionated DX | 0.20 | 0.96 | helper derives from MarksOf<N> and stays type-only |
| Plate and slate-yjs migration backbone | 0.15 | 0.94 | value-first generics plan and no collab/runtime change |
| Regression-proof testing strategy | 0.20 | 0.92 | named type contracts and example typecheck gates |
| Research evidence completeness | 0.15 | 0.92 | live source, prior generics plan, issue corpus |
| shadcn-style composability/minimalism | 0.10 | 0.95 | no UI/product API; examples stay tiny |
Weighted score: 0.946.
Threshold result: pass.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| 1. Current-state read and initial score | complete | live example/core/test reads | accepted additive helper target | none | closure |
| 2. Related issue discovery | complete | #5075 ledger/live rows read | classify as future Improves, not Fixes | no exact closure proof | closure |
| 3. Issue-ledger pass | complete | requirements, issue clusters, coverage matrix, dossier | no fixed issue claim | #5075 proof awaits execution | closure |
| 4. Intent/boundary and decision brief | complete | boundary and decision sections | reject local helper and generic utility | none | closure |
| 5. Research/ecosystem/live-source refresh | complete | live source plus prior generics plan | no external runtime research needed | none | closure |
| 6. Pressure passes | complete | review matrix and hard cuts | helper is type-only | none | closure |
| 7. Maintainer objection ledger | complete | objection row | keep tiny type-only helper | none | closure |
| 8. High-risk deliberate mode | complete | pre-mortem/proof plan | public API proof added | none | closure |
| 9. Ecosystem maintainer pass | skipped | no runtime/plugin/collab behavior change | no Plate/slate-yjs adapter work | none | closure |
| 10. Revision pass | complete | rejected alternatives and API target | add BooleanMarksOf with key helper | none | closure |
| 11. Issue sync accounting | complete | #5075 sync/coverage rows read | no ledger claim until execution proof | none | closure |
| 12. Closure score and final gates | complete | scorecard, handoff, completion gates | ready for Ralph execution | none | none |
BooleanMarksOf<N> alongside BooleanMarkKeysOf<N> because key helper
alone leaves the active-marks example noisy.MarkKeysOf unchanged to avoid violating existing optional-mark
fallback tests.Fixes to execution-time Improves unless
exact original repro proof is added.None for planning.
What would change the decision:
BooleanMarksOf<N> cannot typecheck cleanly against MarksOf<N>, keep
only BooleanMarkKeysOf<N> and use Partial<Pick<...>> in examples.BooleanMarkKeysOf<N> and BooleanMarksOf<N>.BooleanTextKey with Slate-owned helper.BooleanTextKey copied in example -> BooleanMarkKeysOf<N> in
Slate core; status add; proof Live Current State.Partial<Pick<CustomText, CustomTextKey>> -> optional
BooleanMarksOf<N> helper; status add; proof Decision Brief.MarkKeysOf<ParagraphElement> stays unknown; status
keep; proof generic-value-contract.ts.custom-types.d.ts owns mapped conditional type -> imports
Slate helper; status revise; proof Public API Target.keep; proof Internal Runtime Target.keep; proof
Migration Backbone.gate; proof
Legacy Regression Proof Matrix.Improves only after execution proof, not
Fixes; status gate; proof Issue Accounting.BooleanKeys<T>, no MarkKeysOf semantic change, no
example-local helper; status cut; proof Hard Cuts.Status: done.
Goal:
BooleanMarkKeysOf and BooleanMarksOf helpers,
examples use them instead of local BooleanTextKey plumbing,
issue/reference docs stay synced, and focused plus broad checks pass.Current pass:
verification-sweep-passExecution checkpoints:
BooleanMarkKeysOf<N> and BooleanMarksOf<N> in
.tmp/slate-v2/packages/slate/src/interfaces/text.ts.BooleanTextKey with BooleanMarkKeysOf<CustomText>.Partial<Pick<...>> with
BooleanMarksOf<CustomText>.Improves, not Fixes.BooleanMarksOf<N> to pick the exported string
key helper shape.Execution verification:
.tmp/slate-v2: bunx tsc --project packages/slate/test/tsconfig.generic-types.json --noEmit --pretty false red before helper..tmp/slate-v2: bunx tsc --project packages/slate/test/tsconfig.generic-types.json --noEmit --pretty false pass after helper..tmp/slate-v2: bun --filter slate typecheck pass..tmp/slate-v2: bun typecheck:site pass..tmp/slate-v2: rg "BooleanTextKey" site/examples/ts no matches..tmp/slate-v2: bun lint:fix pass; Biome fixed one file..tmp/slate-v2: bun check pass; lint, package/site/root typecheck,
1008 Bun tests, and 267 Slate React Vitest tests passed.plate-2: pnpm lint:fix pass; no fixes applied.