@sanity/id-utils
Type-safe utilities for validating, converting, and generating Sanity document IDs across draft, published, and version variants.
Repository Health
Technical Analysis
Sanity document IDs carry implicit meaning that a plain string type can’t express: a given ID might be a published document, a draft (drafts.-prefixed), or a specific version (versions.<name>.-prefixed), and passing the wrong variant into an API that expects a specific one is a common source of subtle bugs. @sanity/id-utils fixes this at compile time by wrapping IDs in branded TypeScript types (PublishedId, DraftId, VersionId, VariantDefinitionId) built on ts-brand, so a function that only accepts a PublishedId will reject a DraftId with a type error rather than failing at runtime.
Beyond the type-level guarantees, the library validates ID strings at construction time, converts between the draft/published/version forms of the same document, checks whether two IDs point at the same underlying document regardless of variant, and generates new, safe IDs from arbitrary input strings (stripping unsafe characters and capping length) or from a random UUID when no input is given.
What You Get
- Branded ID types —
PublishedId,DraftId,VersionId, andVariantDefinitionIdas distinct TypeScript types built onts-brand, plus the unionDocumentIdtype - Runtime validators — constructor functions (
DraftId(id),PublishedId(id), etc.) that throw descriptiveAggregateErrors when a string doesn’t match the expected ID shape - Converters —
getDraftId,getPublishedId, andgetVersionIdto move an ID between its draft, published, and versioned forms without manual string surgery - Predicates —
isDraftId,isPublishedId,isVersionId,isDraftOf,isVersionOf, andisPublishedIdEqualas TypeScript type guards for narrowing and equality checks across variants - ID generators —
createDraftId,createPublishedId,createVersionId, andcreateVariantDefinitionId, which sanitize an input string or fall back to a random UUID
Common Use Cases
- Enforcing at the type level that a component or function only ever receives a published document ID, not an accidental draft ID
- Converting a draft ID back to its published form before writing a public-facing reference
- Comparing a draft and a published document to confirm they represent the same underlying content
- Generating a safe, deterministic document ID from an external system’s identifier when importing content into Sanity
Under The Hood
Architecture
The package is organized into five small, single-purpose modules — brands.ts defines the branded types and their runtime validators, converters.ts implements the draft/published/version transformations, create.ts generates new sanitized IDs, predicates.ts supplies type-guard functions, and constants.ts centralizes the ID prefixes and validation regex — with index.ts re-exporting the public surface. There’s no class hierarchy or internal state; every function is a pure transformation over a string, and the helpers.ts module supplies a tiny safe()/partition() pair used to aggregate multiple validation attempts (e.g. DocumentId() tries published, draft, and version validators in sequence and reports all failures together via AggregateError if none succeed). Changing the ID grammar in constants.ts (prefixes, separators, the VALID_ID regex) would ripple through validation, conversion, and generation uniformly, since all three consume the same constants.
Tech Stack
Written in TypeScript and built with @sanity/pkg-utils (pkg build) into dual ESM/CJS output plus type declarations, targeting Node 18+. Runtime dependencies are minimal and deliberate: ts-brand for the branded-type helper, @sanity/uuid for random ID generation, and lodash for deburr (accent-stripping) and partition. Linting runs through ESLint with the typescript-eslint and @sanity/prettier-config presets; API documentation is generated via Microsoft’s api-extractor/api-documenter toolchain into the checked-in docs/ folder.
Code Quality
Tests are written with Vitest, including a dedicated .test-d.ts file that exercises TypeScript’s type-checking directly (vitest run --typecheck) alongside conventional runtime assertion tests across five test files covering brands, converters, creation, and predicates. Error handling is explicit and typed — validators throw Error instances with specific, actionable messages, and DocumentId()’s combined validator aggregates all three variant-validator failures into one AggregateError rather than swallowing detail. CI (GitHub Actions) runs lint, typecheck, and test workflows separately on every change, and release-please automates versioned releases from conventional commits.
What Makes It Unique The library’s core idea — using TypeScript’s branded/nominal types to make an otherwise-stringly-typed ID scheme statically distinguishable by variant — is a well-established pattern, but it’s applied here to a narrow, real problem specific to Sanity’s content model (draft vs. published vs. version document IDs), which is exactly the kind of internal convention that’s normally left undocumented and error-prone until it breaks in production.