Browse documentation

State

@typed/id: generate and validate identifiers

Create branded IDs through Effect, validate them at boundaries, and make generation deterministic in tests.

@typed/id provides schemas for identifier formats and Effect-based generators. Generate an ID in the command that creates an entity, store it with that entity, and reuse it for rendering and later updates. Do not generate identity from a template projection.

pnpm add @typed/id effect

Create an entity with the shared Ids facade

Ids is the application facade. Its requirements bubble through a command until the runtime provides them, so creation stays explicit and testable.

import { Effect } from "effect"
import { Ids } from "@typed/id/Ids"

type Issue = { readonly id: string; readonly title: string }

const createIssue = Effect.fn(function* (title: string) {
  const id = yield* Ids.uuid7

  return { id, title } satisfies Issue
})

const program = createIssue("Document @typed/id").pipe(Effect.provide(Ids.Default))

const issue = await Effect.runPromise(program)

Provide Ids.Default once around the application or feature that shares IDs. Its UUIDv7 facade shares one lazy Uuid7State for that Layer, preserving local sequence state. This is local monotonicity, not a distributed ordering or authorization guarantee.

Choose a focused generator when the facade is unnecessary

Use a focused module when one boundary needs one format and no application-wide generator service.

import { Effect } from "effect"
import { uuid7, Uuid7State } from "@typed/id/Uuid7"

const id = await Effect.runPromise(uuid7.pipe(Effect.provide(Uuid7State.Default)))

uuid7 requires Uuid7State; the facade is preferable when several commands must share its state. The API reference lists UUIDv4, UUIDv5, UUIDv7, ULID, KSUID, NanoId, CUID, their schemas, and their focused dependencies.

Choose a format by its dependency

NeedFocused moduleDependency or contract
Random UUIDUuid4RandomValues
Deterministic nameUuid5explicit namespace and name
Time-bearing UUIDUuid7shared Uuid7State
Time-bearing stringUlid or Ksuidtime and entropy
Compact random stringNanoIdentropy

Use Ids when the feature shares these dependencies; use a focused generator at a narrow boundary.

Decode external IDs through a schema

Brands prevent accidental format mixing in TypeScript. A schema also validates an ID arriving from JSON, a URL, or storage.

import { Schema } from "effect"
import { Uuid7 } from "@typed/id/Uuid7"

const Issue = Schema.Struct({ id: Uuid7, title: Schema.String })
const decodeIssue = Schema.decodeUnknownEffect(Issue)

const issue = decodeIssue({
  id: "018f3c8a-4c00-7000-8000-000000000001",
  title: "Document @typed/id",
})

A UUID format is not a domain type or permission check. Add a domain distinction when two entities must not mix, then perform normal authorization after decoding.

Make generation deterministic in tests

IdsTest is deliberately separate from production imports. Each test Layer supplies fixed time, seeded entropy, and fresh UUIDv7 sequence state.

import { Effect } from "effect"
import { expect } from "@effect/vitest"
import { Ids } from "@typed/id/Ids"
import { IdsTest } from "@typed/id/IdsTest"

const pair = Effect.fn(function* () {
  return [yield* Ids.uuid7, yield* Ids.uuid7] as const
})

const first = await Effect.runPromise(pair().pipe(Effect.provide(IdsTest({ currentTime: 0 }))))
const repeated = await Effect.runPromise(pair().pipe(Effect.provide(IdsTest({ currentTime: 0 }))))

expect(first[0]).not.toBe(first[1])
expect(first[0]).toBe(repeated[0])

The first assertion proves sequence state advances within one Layer; the second proves an identical fresh Layer reproduces the sequence. currentTime fixes DateTimes; advancing the TestClock provided by IdsTest does not advance that fixed time service. Provide a custom DateTimes Layer when a test needs generator time to change.

Carry identity through acknowledgement

Keep a client-generated entity key when a server later returns a persistent ID. Replacing the rendering key makes acknowledgement look like deleting and remounting a row, which can discard focus or local draft state. Store the server ID beside the client key instead.

This matters for keyed rows and optimistic edits. If identity changes unexpectedly, trace the creation command and count generator executions. Deterministic layers make equal generator-call sequences comparable; they do not make different programs consume the same IDs.