- Schema Type System
- Separate Decode/Encode Services
- Explicit JSON Serialization
- First-Class Transformations
- First-Class Filters
- Constructor API
- Integrated Optics
- Enhanced STM Collections
- Breaking Changes
- Migration Guide
v4 schemas track 14 type parameters instead of v3's 6-8, preserving complete type information through composition.
v3:
interface Schema<A, I = A, R = never>v4:
interface Bottom<
T, // Decoded type
E, // Encoded type
RD, // Decode services
RE, // Encode services
Ast, // AST node
RebuildOut, // Type after modification
TypeMakeIn, // Constructor input
Iso, // Optic focus
TypeParameters, // Generic params
TypeMake, // Constructor type
TypeMutability, // readonly | mutable
TypeOptionality, // required | optional
TypeConstructorDefault, // no-default | with-default
EncodedMutability, // Encoded mutability
EncodedOptionality // Encoded optionality
>This eliminates type information loss when composing schemas. Mutability, optionality, and constructor defaults are now tracked at the type level.
Real impact: Schema derivation APIs (like mapFields, pick, omit) preserve exact type characteristics, enabling
safer refactoring and more precise TypeScript autocomplete.
v3 used a single R parameter for all service dependencies. v4 splits this into RD (decode requirements) and RE
(encode requirements).
v3:
// Both encode and decode require Database service
declare const UserSchema: Schema<User, string, Database>v4:
// Only decode requires Database; encode needs nothing
declare const UserSchema: Codec<User, string, Database, never>
// Decoding: Effect<User, SchemaError, Database>
const decode = Schema.decodeEffect(UserSchema)("user-123")
// Encoding: Effect<string, SchemaError, never>
const encode = Schema.encodeEffect(UserSchema)(user)This enables better tree-shaking and eliminates false service dependencies.
Real impact: API responses no longer require database services just because request parsing does. Cleaner Layer dependency graphs and smaller production bundles.
v3 provided implicit JSON serialization. v4 requires explicit codec creation.
v3:
// Implicit JSON handling
const serialized = Schema.encode(schema)(value)v4:
// Explicit JSON codec
const codec = Schema.makeSerializerJson(schema)
const serialized = codec.serialize(value)
// Helper for JSON strings
const schema = Schema.fromJsonString(Schema.Number)The explicit API reduces bundle size by requiring opt-in for JSON features.
Real impact: Serverless functions and Docker images are 50-100KB smaller. Faster cold starts for Lambda/Cloud Functions.
v3 embedded transformations in schemas. v4 makes them standalone, composable objects.
v3:
const schema = Schema.transform(
Schema.Number,
Schema.Date,
(n) => new Date(n),
(d) => d.getTime()
)v4:
const transform = Transformation.transform({
decode: (epochMillis: number) => new Date(epochMillis),
encode: (date: Date) => date.getTime()
})
const DateFromEpochMillis = Schema.Date.pipe(Schema.encodeTo(Schema.Number, transform))
// Transformations are inspectable and reusable
console.log(transform) // Shows decode/encode logicTransformation objects can be tested independently and composed like optics.
Real impact: Share transformation logic across schemas (e.g., date parsing, ID lookups). Unit test transformations without schema overhead.
v3 baked validation into schema definitions. v4 separates filters as composable objects with multi-issue reporting.
v3:
const schema = Schema.Number.pipe(Schema.positive, Schema.int)v4:
const positiveInt = Schema.Number.pipe(
Schema.filter(Schema.positive({ errors: "all" })),
Schema.filter(Schema.int({ errors: "all" }))
)
// Reports all validation failures at once
const result = Schema.decodeUnknownSync(positiveInt)(-3.14)
// Error: Expected positive number, expected integerThe errors: "all" option collects all validation failures instead of stopping at the first error.
Real impact: API clients fix all validation errors in one request instead of multiple round trips. Better UX with complete error context.
v3 had mixed validation behavior in constructors. v4 introduces explicit makeUnsafe() with clear error handling.
v3:
const value = schema.make(input) // May or may not validatev4:
try {
const value = schema.makeUnsafe(input)
} catch (e) {
// Error is in e.cause as SchemaError
console.error(e.cause)
}
// For Effect context
const program = Effect.gen(function* () {
const value = yield* schema.make(input)
return value
})The makeUnsafe name makes validation explicit. For Effect-based code, use schema.make() which returns
Effect<A, SchemaError>.
Real impact: No more wondering if a constructor validates. Clear separation between throwing (makeUnsafe) and
Effect-based (make) constructors.
v3 treated optics as a separate concern. v4 derives optics directly from schemas.
v4:
import { Optic } from "effect"
const employee = {
company: {
address: {
street: {
name: "main street"
}
}
}
}
// Auto-derived from schema structure
const updated = Optic.id<Employee>().key("company").key("address").key("street").key("name").modify(String.capitalize)(
employee
)
// Result: { company: { address: { street: { name: "Main Street" } } } }Type-safe lenses with full autocomplete work seamlessly with schema definitions.
Real impact: Update deeply nested API response structures without manual spread operators. Type-safe field updates with compiler-enforced correctness.
v4 adds comprehensive transactional collections for lock-free concurrent state management.
v4 New Collections:
import { TxHashMap, TxQueue, Effect } from "effect"
// Transactional hash map
const program = Effect.gen(function* () {
const map = yield* TxHashMap.make<string, number>()
yield* Effect.atomic(
Effect.gen(function* () {
yield* TxHashMap.set(map, "a", 1)
yield* TxHashMap.set(map, "b", 2)
const sum =
(yield* TxHashMap.get(map, "a")).pipe(Option.getOrElse(() => 0)) +
(yield* TxHashMap.get(map, "b")).pipe(Option.getOrElse(() => 0))
if (sum > 10) {
return yield* Effect.interrupt // Rolls back entire transaction
}
return sum
})
)
})All operations compose atomically with Effect.atomic(). Available collections: TxHashMap, TxHashSet, TxQueue,
TxChunk, TxSemaphore.
Real impact: Build lock-free concurrent systems (rate limiters, caches, job queues) without explicit mutex management. Automatic rollback on errors.
Removed zipWith and ap. Use all() for parallel composition:
// v3
const result = Result.zipWith(a, b, (x, y) => x + y)
// v4
const result = Result.all([a, b]).pipe(Result.map(([x, y]) => x + y))Now throws the error directly instead of wrapping it:
// v3
try {
const value = Result.getOrThrow(result)
} catch (wrapped) {
const error = wrapped.error // Wrapped
}
// v4
try {
const value = Result.getOrThrow(result)
} catch (error) {
// Error thrown directly
}- Replace implicit JSON usage:
// v3
const encoded = Schema.encode(schema)(value)
// v4
const codec = Schema.makeSerializerJson(schema)
const encoded = codec.serialize(value)- Update constructors:
// v3
const value = schema.make(input)
// v4 (throws on error)
const value = schema.makeUnsafe(input)
// v4 (Effect-based)
const value = yield * schema.make(input)- Separate decode/encode services:
// v3
Schema<A, I, R>
// v4
Codec<A, I, RD, RE>Replace removed methods:
// v3
Result.zipWith(a, b, fn)
Result.ap(fab, fa)
// v4
Result.all([a, b]).pipe(Result.map(([x, y]) => fn(x, y)))
Effect.all([fa, fb]).pipe(Effect.map(([a, b]) => fn(a, b)))These modules have stable APIs across v3 and v4:
- Array, BigDecimal, BigInt, Boolean, Brand, Chunk
- Iterable, Number, Order, Ordering, Predicate, Record
- Redacted, RegExp, String, Struct, Symbol, Tuple
Core Effect concepts remain unchanged:
Effect<A, E, R>monad- Fiber-based concurrency
- Layer dependency injection
- TestClock for deterministic testing
- Stream/Channel architecture