TypeScript Integration
modern-cmdk is built with TypeScript 7.1.0-dev.20261107.1 and provides first-class type safety.
Branded IDs
Item and group identifiers use branded types to prevent accidental mixing:
import { groupId, itemId } from 'modern-cmdk';
import type { GroupId } from 'modern-cmdk';
const myItem = itemId('copy'); // type: ItemId (branded string)
const myGroup = groupId('edit'); // type: GroupId (branded string)
// Type error — cannot assign ItemId to GroupId
// @ts-expect-error TS2322: ItemId is not assignable to GroupId
const wrong: GroupId = myItem;Type-Safe Machine Options
The createCommandMachine options are fully typed, so callback parameters are inferred:
import { createCommandMachine, itemId } from 'modern-cmdk';
const machine = createCommandMachine({
items: [
{ id: itemId('copy'), value: 'Copy' },
],
onSelect: (id) => {
// `id` is inferred as ItemId, not string
console.log(id);
},
});Disposable Pattern
The machine implements Disposable for the using keyword (ESNext explicit resource management):
import { createCommandMachine, itemId } from 'modern-cmdk';
import type { CommandItem } from 'modern-cmdk';
const items: readonly CommandItem[] = [{ id: itemId('copy'), value: 'Copy' }];
{
using machine = createCommandMachine({ items });
// machine is automatically disposed when leaving this block
}Readonly State
All state objects are deeply immutable at the type level:
import { createCommandMachine, itemId } from 'modern-cmdk';
const machine = createCommandMachine();
const state = machine.getState();
// @ts-expect-error TS2540: readonly property
state.search = 'new';
// @ts-expect-error TS2339: readonly array has no push
state.filteredIds.push(itemId('x'));isolatedDeclarations
All packages are built with isolatedDeclarations: true, ensuring .d.ts files are generated without full type inference — faster builds and compatible with any bundler.
erasableSyntaxOnly
The codebase uses only erasable TypeScript syntax (no enums, no namespaces, no constructor parameter properties). This means any tool that strips types works correctly: Node's built-in type stripping first, then esbuild, swc and Bun.
Node runs .ts files by stripping the types, with no flag, from v23.6.0 and v22.18.0 (type stripping is stable from v25.2.0 and v24.12.0), per nodejs.org/api/typescript.html, read on 2026-11-04. On the Node pinned in .nvmrc (28.0.0-nightly20261107a43bfb5bb2):
node -p process.features.typescript
# stripstrip is the default value; it is false when Node runs with --no-strip-types (nodejs.org/api/process.html). Stripping handles only erasable syntax, which is why this codebase restricts itself to it.