schema-composition

from front-depiction/claude-setup

Reusable Claude Code configuration for Effect TypeScript projects with specialized agents and skills

10 stars4 forksUpdated Jan 19, 2026
npx skills add https://github.com/front-depiction/claude-setup --skill schema-composition

SKILL.md

Schema Composition Skill

Expert guidance for composing, transforming, and validating data with Effect Schema.

Core Concepts

The Schema Type

Every schema in Effect has the type signature Schema<Type, Encoded, Context> where:

  • Type: The validated, decoded output type (what you get after successful decoding)
  • Encoded: The raw input type (what you provide for decoding)
  • Context: External dependencies required for encoding/decoding (often never)

Example:

import { Schema } from "effect"

// Schema<number, string, never>
//        ^Type  ^Encoded ^Context
const NumberFromString = Schema.NumberFromString

Decoding vs Encoding

  • Decoding: Transform EncodedType (e.g., string "123" → number 123)
  • Encoding: Transform TypeEncoded (e.g., number 123 → string "123")

Effect Schema follows "parse, don't validate" - schemas transform data into the desired format, not just check validity.

Schema.compose vs Schema.pipe

Understanding when to use compose vs pipe is fundamental to schema composition.

Schema.compose - Chaining Transformations

Use Schema.compose to chain schemas with different types at each stage. It connects the output type of one schema to the input type of another.

Type Signature:

Schema.compose: <A, B, R1>(from: Schema<B, A, R1>) =>
  <C, R2>(to: Schema<C, B, R2>) => Schema<C, A, R1 | R2>

When to Use:

  • Multi-step transformations where each stage changes the type
  • Connecting parsing and validation steps
  • Building pipelines from Encoded → Intermediate → Type

Example - Parse and Validate:

import { Schema } from "effect"

// Split string → array, then transform array → numbers
const schema = Schema.compose(
  Schema.split(","),              // string → readonly string[]
  Schema.Array(Schema.NumberFromString) // readonly string[] → readonly number[]
)

// Result: Schema<readonly number[], string, never>
console.log(Schema.decodeUnknownSync(schema)("1,2,3")) // [1, 2, 3]

Example - Boolean from String via Literal:

import { Schema } from "effect"

const BooleanFromString = Schema.compose(
  Schema.Literal("on", "off"),  // string → "on" | "off"
  Schema.transform(
    Schema.Literal("on", "off"),
    Schema.Boolean,
    {
      strict: true,
      decode: (s) => s === "on",
      encode: (b) => b ? "on" : "off"
    }
  )
)

Non-strict Composition:

When type boundaries don't align perfectly, use { strict: false }:

import { Schema } from "effect"

// Without strict: false, TypeScript error
Schema.compose(
  Schema.Union(Schema.Null, Schema.Literal("0")),
  Schema.NumberFromString,
  { strict: false }
)

Schema.pipe - Sequential Refinements

Use Schema.pipe to apply filters and refinements to the same type. It doesn't change the type, just adds validation constraints.

When to Use:

  • Adding validation rules to an existing schema
  • Chaining multiple filters on the same type
  • Refining without transformation

Example - Number Validation:

import { Schema } from "effect"

const PositiveInt = Schema.Number.pipe(
  Schema.int(),      // Ensure it's an integer
  Schema.positive()  // Ensure it's positive
)

// Type: Schema<number, number, never>
// Both Type and Encoded are `number`

Example - String Validation:

import { Schema } from "effect"

const ValidEmail = Schema.String.pipe(
  Schema.trimmed(),
  Schema.lowercased(),
  Schema.minLength(5),
  Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
)

Key Differences

AspectSchema.composeSchema.pipe
PurposeChain transformationsApply refinements
Type ChangeChanges type at each stageType stays the same
Examplestring → array → numbersnumber → positive number
Use CaseMulti-step parsingValidation constraints

Built-in Filters

Filters add validation constraints without changing the schema's type. They use Schema.filter() under the hood.

String Filters

import { Schema } from "effect"

// Length constraints
Schema.String.pipe(Schema.maxLength(5))
Schema.String.pipe(Schema.minLength(5))
Schema.String.pipe(Schema.nonEmptyString()) // alias: Schema.NonEmptyString
Schema.String.pipe(Schema.length(5))
Schema.String.pipe(Schema.length({ min: 2, max: 4 }))

// Pattern matching
Schema.String.pipe(Schema.pattern(/^[a-z]+$/))
Schema.String.pipe(Schema.startsWith("prefix"))
Schema.String.pipe(Schema.endsWith("suffix"))
Schema.String.pipe(Schema.includes("substring"))

// Case and whitespace validation
Schema.String.pipe(Schema.trimmed())        // No leading/trailing whitespace
Schema.String.pipe(Schema.lowercased())     // All lowercase
Schema.String.pipe(Schema.uppercased())     // All uppercase
Schema.String.pipe(Schema.capitalized())    // First letter capitalized
Schema.String.pipe(Schema.uncapitalize

...
Read full content

Repository Stats

Stars10
Forks4