xiaohongshu
xiaohongshu
effect-schema-mastery
Schema.TaggedStruct, Schema.TaggedClass, branded types, refinements, transformations. Pattern matching on _tag. Covers runtime validation, type inference, and schema composition for Effect-TS.
Full skill instructions
Effect Schema is the runtime validation and type inference system for Effect-TS. In TMNL, all domain types MUST be defined as Schemas (not raw TypeScript interfaces).
This enables:
CRITICAL DOCTRINE (from CLAUDE.md):
Use Effect Schema instead of raw TypeScript interfaces/types. This is non-negotiable for domain types.
../../submodules/effect/packages/schema/src/
Schema.ts — Core schema constructorsTaggedStruct.ts — Discriminated union data structuresTaggedClass.ts — Entities with methods../../submodules/website/content/src/content/docs/docs/schema/
.mdx files on schema usage, transformations, validationsrc/lib/search/schemas.ts (Schema.Struct, refinements, filters)src/lib/search/query/schemas.ts (Literal enums, tagged unions)src/lib/ams/v2/base/events/schema.ts (EventLog integration)Need to define a type?
│
├─ Discriminated data (events, messages, commands)?
│ └─ Use: Schema.TaggedStruct("Tag", { fields })
│ Pattern matching on _tag
│
├─ Entity with methods (domain objects)?
│ └─ Use: Schema.TaggedClass<T>()("Tag", { fields })
│ Methods + data in one class
│
├─ Enum-like values?
│ └─ Use: Schema.Literal("a", "b", "c")
│ Runtime + type representation
│
├─ Constrained primitives (non-empty string, positive int)?
│ └─ Use: Schema.filter(predicate)
│ Refinement types
│
├─ Bidirectional transformation (Date ↔ ISO string)?
│ └─ Use: Schema.transform(encode, decode)
│ Type-safe conversions
│
└─ Unique identifiers (UserId, OrderId)?
└─ Use: Schema.brand("BrandName")
Branded primitives for type safety
When to use:
_tag field for type narrowingSignature:
const MyData = Schema.TaggedStruct("MyData", {
field1: Schema.String,
field2: Schema.Number,
})
type MyData = typeof MyData.Type
// { readonly _tag: "MyData"; readonly field1: string; readonly field2: number }
Full Example:
import { Schema } from 'effect'
// Define tagged structs
const UserCreated = Schema.TaggedStruct('UserCreated', {
userId: Schema.String,
email: Schema.String,
timestamp: Schema.DateFromSelf,
})
const UserDeleted = Schema.TaggedStruct('UserDeleted', {
userId: Schema.String,
reason: Schema.optional(Schema.String),
timestamp: Schema.DateFromSelf,
})
// Union for pattern matching
const UserEvent = Schema.Union(UserCreated, UserDeleted)
type UserEvent = typeof UserEvent.Type
// Pattern match on _tag
function handleEvent(event: UserEvent) {
switch (event._tag) {
case 'UserCreated':
console.log(`User ${event.userId} created at ${event.timestamp}`)
return startOnboarding(event.email)
case 'UserDeleted':
console.log(`User ${event.userId} deleted: ${event.reason ?? 'no reason'}`)
return cleanupData(event.userId)
}
}
// Runtime validation
const parseEvent = Schema.decodeUnknownSync(UserEvent)
const event = parseEvent({ _tag: 'UserCreated', userId: '123', email: '[email protected]', timestamp: new Date() })
Key Features:
_tag: Discriminator field for pattern matchingreadonlytypeof Schema.Type gives you the TypeScript typeSchema.UnionTMNL Example (src/lib/search/query/schemas.ts):
const ExactMatch = Schema.TaggedStruct('ExactMatch', {
query: Schema.String,
})
const RegexMatch = Schema.TaggedStruct('RegexMatch', {
pattern: Schema.String,
flags: Schema.optional(Schema.String),
})
const QueryTerm = Schema.Union(ExactMatch, RegexMatch)
type QueryTerm = typeof QueryTerm.Type
// Pattern matching
function executeQuery(term: QueryTerm) {
switch (term._tag) {
case 'ExactMatch':
return index.search(term.query)
case 'RegexMatch':
return index.searchRegex(new RegExp(term.pattern, term.flags))
}
}
When to use:
Signature:
class EntityName extends Schema.TaggedClass<EntityName>()(
"EntityName",
{
field1: Schema.String,
field2: Schema.Number,
}
) {
// Methods here
get computed() { return this.field1 + this.field2 }
withField1(value: string) { return new EntityName({ ...this, field1: value }) }
}
Full Example:
import { Schema } from 'effect'
class User extends Schema.TaggedClass<User>()(
'User',
{
id: Schema.String,
name: Schema.NonEmptyString,
email: Schema.String,
createdAt: Schema.DateFromSelf,
}
) {
// Computed properties
get displayName() {
return `${this.name} <${this.email}>`
}
// Methods (immutable updates)
withName(name: string) {
return new User({ ...this, name })
}
withEmail(email: string) {
return new User({ ...this, email })
}
// Domain logic
isActive(now: Date = new Date()) {
const daysSinceCreation = (now.getTime() - this.createdAt.getTime()) / (1000 * 60 * 60 * 24)
return daysSinceCreation <= 30
}
}
// Usage
const user = new User({
id: '123',
name: 'Alice',
email: '[email protected]',
createdAt: new Date(),
})
console.log(user._tag) // "User"
console.log(user.displayName) // "Alice <[email protected]>"
console.log(user.isActive()) // true
const updated = user.withName('Alice Smith')
Key Features:
new ClassName(data) syntaxnew ClassName({ ...this, field: newValue })Schema.Array(User), Schema.Union, etc.TMNL Pattern (Schema.Class for entities):
import { Schema } from 'effect'
class GridColumn extends Schema.Class<GridColumn>('GridColumn')({
field: Schema.String,
headerName: Schema.String,
width: Schema.optional(Schema.Number),
sortable: Schema.optional(Schema.Boolean),
}) {
get displayWidth() {
return this.width ?? 150
}
withWidth(width: number) {
return new GridColumn({ ...this, width })
}
isSortable() {
return this.sortable ?? true
}
}
When to use:
type Status = "pending" | "active"Signature:
const StatusSchema = Schema.Literal("pending", "active", "archived")
type Status = typeof StatusSchema.Type // "pending" | "active" | "archived"
Full Example:
import { Schema } from 'effect'
// Define literal schema
const Status = Schema.Literal('pending', 'active', 'archived')
type Status = typeof Status.Type
const Priority = Schema.Literal(1, 2, 3, 4, 5)
type Priority = typeof Priority.Type
// Use in structs
const Task = Schema.Struct({
id: Schema.String,
title: Schema.String,
status: Status,
priority: Priority,
})
// Runtime validation
const decodeTask = Schema.decodeUnknownSync(Task)
const task = decodeTask({
id: '1',
title: 'Fix bug',
status: 'active', // ✅ Valid
priority: 3, // ✅ Valid
})
// This throws:
// decodeTask({ ...task, status: 'invalid' }) // ❌ Not in literal set
Key Features:
type X = "a" | "b" has no runtime representationTMNL Example (src/lib/search/schemas.ts):
export const SearchStrategySchema = Schema.Literal('exact', 'prefix', 'fuzzy', 'auto')
export type SearchStrategy = typeof SearchStrategySchema.Type
export const SearchOptionsSchema = Schema.Struct({
limit: Schema.optional(Schema.Number.pipe(Schema.positive())),
strategy: Schema.optional(SearchStrategySchema),
fuzzyThreshold: Schema.optional(Schema.Number.pipe(Schema.filter((n) => n >= 0 && n <= 1))),
})
When to use:
Signature:
const PositiveInt = Schema.Number.pipe(
Schema.filter((n) => n > 0 && Number.isInteger(n), {
message: () => "Must be a positive integer"
})
)
Full Example:
import { Schema } from 'effect'
// Non-empty string
const NonEmptyString = Schema.String.pipe(
Schema.filter((s) => s.length > 0, {
message: () => "String cannot be empty"
})
)
// Email (simple validation)
const Email = Schema.String.pipe(
Schema.filter((s) => s.includes('@'), {
message: () => "Invalid email format"
})
)
// Score between 0 and 1
const Score = Schema.Number.pipe(
Schema.filter((n) => n >= 0 && n <= 1, {
message: () => "Score must be between 0 and 1"
})
)
// Age (positive integer)
const Age = Schema.Number.pipe(
Schema.filter((n) => n > 0 && Number.isInteger(n), {
message: () => "Age must be a positive integer"
})
)
// Use in struct
const Person = Schema.Struct({
name: NonEmptyString,
email: Email,
age: Age,
})
// Validation
const decodePerson = Schema.decodeUnknownSync(Person)
const person = decodePerson({
name: 'Alice',
email: '[email protected]',
age: 30,
})
// Throws: decodePerson({ name: '', email: '[email protected]', age: 30 })
// Throws: decodePerson({ name: 'Alice', email: 'invalid', age: 30 })
// Throws: decodePerson({ name: 'Alice', email: '[email protected]', age: -5 })
Key Features:
(value) => booleanmessage: () => string.pipe()Built-in refinements:
Schema.NonEmptyString // String with length > 0
Schema.positive() // Number > 0
Schema.negative() // Number < 0
Schema.int() // Integer
Schema.minLength(5) // String/Array with min length
Schema.maxLength(10) // String/Array with max length
Schema.pattern(/^\d{3}-\d{4}$/) // Regex validation
TMNL Example (src/lib/search/schemas.ts):
export const ValidSearchResultSchema = Schema.Struct({
item: Schema.Unknown,
score: Schema.Number.pipe(
Schema.filter((n) => n >= 0 && n <= 1, {
message: () => 'Score must be between 0 and 1',
})
),
matches: Schema.optional(Schema.Array(FieldMatchSchema)),
})
When to use:
Signature:
const UserId = Schema.String.pipe(Schema.brand("UserId"))
type UserId = typeof UserId.Type // string & Brand<"UserId">
Full Example:
import { Schema } from 'effect'
// Define branded types
const UserId = Schema.String.pipe(
Schema.brand("UserId"),
Schema.minLength(1)
)
type UserId = typeof UserId.Type
const OrderId = Schema.String.pipe(
Schema.brand("OrderId"),
Schema.minLength(1)
)
type OrderId = typeof OrderId.Type
const Email = Schema.String.pipe(
Schema.brand("Email"),
Schema.pattern(/@/)
)
type Email = typeof Email.Type
// These are different types!
const userId: UserId = "user-123" as UserId
const orderId: OrderId = "order-456" as OrderId
// Type error: Type 'UserId' is not assignable to type 'OrderId'
// const wrong: OrderId = userId // ❌ Compile error
// Use in functions
function getUser(id: UserId) {
return db.users.find(id)
}
function getOrder(id: OrderId) {
return db.orders.find(id)
}
getUser(userId) // ✅
getOrder(orderId) // ✅
// getUser(orderId) // ❌ Type error
Key Features:
UserId ≠ OrderId even though both are stringsAdvanced: Branded + Refinement
const PositiveInt = Schema.Number.pipe(
Schema.int(),
Schema.positive(),
Schema.brand("PositiveInt")
)
type PositiveInt = typeof PositiveInt.Type
When to use:
Signature:
const MySchema = Schema.transform(
fromSchema,
toSchema,
{
decode: (from) => to,
encode: (to) => from
}
)
Full Example:
import { Schema } from 'effect'
// Date ↔ ISO string
const DateFromString = Schema.transform(
Schema.String,
Schema.DateFromSelf,
{
decode: (s) => new Date(s),
encode: (d) => d.toISOString(),
}
)
// Parse JSON string ↔ object
const JsonString = <A>(schema: Schema.Schema<A>) =>
Schema.transform(
Schema.String,
schema,
{
decode: (s) => JSON.parse(s),
encode: (obj) => JSON.stringify(obj),
}
)
// Normalize whitespace
const TrimmedString = Schema.transform(
Schema.String,
Schema.String,
{
decode: (s) => s.trim(),
encode: (s) => s,
}
)
// Use in struct
const Event = Schema.Struct({
id: Schema.String,
timestamp: DateFromString,
metadata: JsonString(Schema.Struct({ key: Schema.String })),
description: TrimmedString,
})
// Decode from wire format
const decodeEvent = Schema.decodeUnknownSync(Event)
const event = decodeEvent({
id: '123',
timestamp: '2025-01-01T00:00:00.000Z',
metadata: '{"key":"value"}',
description: ' whitespace ',
})
console.log(event.timestamp) // Date object
console.log(event.metadata) // { key: 'value' }
console.log(event.description) // "whitespace" (trimmed)
// Encode to wire format
const encodeEvent = Schema.encodeSync(Event)
const wire = encodeEvent(event)
console.log(wire.timestamp) // "2025-01-01T00:00:00.000Z"
console.log(wire.metadata) // '{"key":"value"}'
Key Features:
decode (parse) and encode (serialize)Schema.Struct, Schema.Array, etc.Built-in transformations:
Schema.DateFromSelf // Date (no transformation)
Schema.DateFromString // string → Date
Schema.NumberFromString // "123" → 123
Schema.parseJson(schema) // JSON string → typed object
When to use:
Full Example:
import { Schema } from 'effect'
const User = Schema.Struct({
id: Schema.String,
name: Schema.String,
email: Schema.String,
age: Schema.Number,
role: Schema.Literal('admin', 'user', 'guest'),
metadata: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
})
type User = typeof User.Type
// Optional fields
const PartialUser = Schema.partial(User)
// Pick fields
const UserSummary = Schema.pick(User, 'id', 'name', 'email')
// Omit fields
const UserWithoutMetadata = Schema.omit(User, 'metadata')
Full Example:
import { Schema } from 'effect'
const StringArray = Schema.Array(Schema.String)
const UserArray = Schema.Array(User)
// Non-empty array
const NonEmptyStringArray = Schema.Array(Schema.String).pipe(
Schema.filter((arr) => arr.length > 0, {
message: () => "Array cannot be empty"
})
)
// Min/max length
const BoundedArray = Schema.Array(Schema.String).pipe(
Schema.minItems(1),
Schema.maxItems(10)
)
Full Example:
import { Schema } from 'effect'
// Record<string, number>
const StringToNumber = Schema.Record({ key: Schema.String, value: Schema.Number })
// Record<string, User>
const UserMap = Schema.Record({ key: Schema.String, value: User })
// Constrained keys
const StatusCounts = Schema.Record({
key: Schema.Literal('pending', 'active', 'archived'),
value: Schema.Number,
})
EventLog requires Schema-backed payloads. This is non-negotiable.
import { Event, EventGroup } from '@effect/experimental'
import { Schema } from 'effect'
// Event payload MUST be Schema
const UserCreatedPayload = Schema.Struct({
id: Schema.String,
name: Schema.NonEmptyString,
email: Schema.String,
createdAt: Schema.DateFromSelf,
})
const UserCreated = Event.make({
tag: 'UserCreated',
primaryKey: (payload) => payload.id,
payload: UserCreatedPayload,
success: Schema.Void,
})
// Use in EventGroup
const userEvents = EventGroup.make({
UserCreated,
// ... other events
})
import { Schema, Effect } from 'effect'
const ApiResponse = Schema.Struct({
status: Schema.Literal('success', 'error'),
data: Schema.optional(Schema.Unknown),
error: Schema.optional(Schema.String),
timestamp: Schema.DateFromString,
})
const fetchUser = (id: string) =>
Effect.gen(function* () {
const response = yield* Effect.tryPromise(() =>
fetch(`/api/users/${id}`).then(r => r.json())
)
// Validate response
const validated = yield* Schema.decodeUnknown(ApiResponse)(response)
if (validated.status === 'error') {
return yield* Effect.fail(new Error(validated.error ?? 'Unknown error'))
}
return validated.data
})
import { Schema } from 'effect'
const ExactQuery = Schema.TaggedStruct('Exact', {
term: Schema.String,
})
const RegexQuery = Schema.TaggedStruct('Regex', {
pattern: Schema.String,
flags: Schema.optional(Schema.String),
})
const RangeQuery = Schema.TaggedStruct('Range', {
field: Schema.String,
min: Schema.Number,
max: Schema.Number,
})
const Query = Schema.Union(ExactQuery, RegexQuery, RangeQuery)
type Query = typeof Query.Type
function executeQuery(query: Query) {
switch (query._tag) {
case 'Exact':
return searchExact(query.term)
case 'Regex':
return searchRegex(query.pattern, query.flags)
case 'Range':
return searchRange(query.field, query.min, query.max)
}
}
// WRONG — No runtime validation
interface User {
id: string
name: string
email: string
}
// CORRECT
const User = Schema.Struct({
id: Schema.String,
name: Schema.String,
email: Schema.String,
})
type User = typeof User.Type
// WRONG — No runtime representation
type Status = "pending" | "active" | "archived"
// CORRECT
const Status = Schema.Literal("pending", "active", "archived")
type Status = typeof Status.Type
_tag for Discriminated Unions// WRONG — Manual _tag field
const UserCreated = Schema.Struct({
_tag: Schema.Literal('UserCreated'),
userId: Schema.String,
})
// CORRECT — TaggedStruct auto-generates _tag
const UserCreated = Schema.TaggedStruct('UserCreated', {
userId: Schema.String,
})
// WRONG — Manual filter
const PositiveNumber = Schema.Number.pipe(
Schema.filter((n) => n > 0)
)
// CORRECT — Built-in refinement
const PositiveNumber = Schema.Number.pipe(Schema.positive())
| Need | Schema Constructor | Example |
|---|---|---|
| Plain object | Schema.Struct({ ... }) | Schema.Struct({ name: Schema.String }) |
| Discriminated union | Schema.TaggedStruct("Tag", { ... }) | Schema.TaggedStruct("UserCreated", { id: Schema.String }) |
| Entity with methods | Schema.TaggedClass<T>()("Tag", { ... }) | class User extends Schema.TaggedClass<User>()("User", { ... }) |
| Enum values | Schema.Literal("a", "b") | Schema.Literal("pending", "active") |
| Array | Schema.Array(itemSchema) | Schema.Array(Schema.String) |
| Dictionary | Schema.Record({ key, value }) | Schema.Record({ key: Schema.String, value: Schema.Number }) |
| Optional field | Schema.optional(schema) | Schema.optional(Schema.String) |
| Nullable | Schema.NullOr(schema) | Schema.NullOr(Schema.String) |
| Refinement | schema.pipe(Schema.filter(...)) | Schema.Number.pipe(Schema.positive()) |
| Branded type | schema.pipe(Schema.brand("Name")) | Schema.String.pipe(Schema.brand("UserId")) |
| Transformation | Schema.transform(from, to, { decode, encode }) | Schema.DateFromString |
| Union | Schema.Union(A, B, C) | Schema.Union(ExactQuery, RegexQuery) |
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience