Creating ADTs
Scaffold ADT (Algebraic Data Type) unions with discriminated member types.
CRITICAL
Read the Conventions First
Before creating any ADT, read the convention documents:
~/.claude/docs/conventions/namespace-module.md— Core_.ts/__.tspattern~/.claude/docs/conventions/library-adt.md— ADT-specific patterns
Operations
Create ADT Union
Structure:
src/lib/<adt-name>/
├── _.ts # export * as AdtName from './__.js'
├── __.ts # exports member namespaces + union
├── <adt-name>.ts # Union definition
├── <member-a>.ts # Member type
└── <member-b>.ts # Member type
Steps:
- Create directory at
src/lib/<adt-name>/(kebab-case matching ADT name) - Create member files with tagged structs
- Create union file importing members directly (NOT from
__.ts) - Create
__.tsexporting members as namespaces + union exports - Create
_.tspointing to__.ts - Add package.json imports and tsconfig.json paths
Member File Pattern (Effect Schema)
// <member>.ts
import * as S from 'effect/Schema'
export class MemberName extends S.TaggedClass<MemberName>()('AdtNameMemberName', {
// fields...
}) {
static is = S.is(MemberName)
}
Naming Rules:
- Class name = member name (short, local):
Versioned - Tag name = fully qualified:
DocumentVersioned
Union File Pattern
// <adt-name>.ts
import * as S from 'effect/Schema'
import { MemberA } from './member-a.js' // Direct import, NOT from __.ts
import { MemberB } from './member-b.js'
export const AdtName = S.Union(MemberA, MemberB)
export type AdtName = typeof AdtName.Type
Barrel File Pattern (__.ts)
// __.ts
export * from './<adt-name>.js'
export * as MemberA from './member-a.js'
export * as MemberB from './member-b.js'
Examples
LifecycleEvent ADT
// added.ts
export class Added extends S.TaggedClass<Added>()('LifecycleEventAdded', {
schema: SchemaRef,
revision: Revision,
}) {
static is = S.is(Added)
}
// removed.ts
export class Removed extends S.TaggedClass<Removed>()('LifecycleEventRemoved', {
schema: SchemaRef,
revision: Revision,
}) {
static is = S.is(Removed)
}
// lifecycle-event.ts
import { Added } from './added.js'
import { Removed } from './removed.js'
export const LifecycleEvent = S.Union(Added, Removed)
export type LifecycleEvent = typeof LifecycleEvent.Type
// __.ts
export * as Added from './added.js'
export * from './lifecycle-event.js'
export * as Removed from './removed.js'
// _.ts
export * as LifecycleEvent from './__.js'
Consumer Usage
// Import ONLY from namespace (_.js)
import { LifecycleEvent } from '#lifecycle-event'
// Access members via namespace
const added = LifecycleEvent.Added.make({ schema, revision })
const removed = LifecycleEvent.Removed.make({ schema, revision })
// Type check
if (LifecycleEvent.Added.is(event)) {
// event is Added
}
Notes
- Union file imports members directly to avoid circular dependencies
- Consumer code imports ONLY from
_.js, never from__.js - Use
.make()constructor from tagged classes, never manual object construction - Tag names should be fully qualified (
AdtNameMemberName) for global uniqueness - See convention doc for Simple ADT Pattern (alternative when full structure is overkill)
微信扫一扫