Data
Data works in VibeLang exactly as in Effect. Use it for classes, tagged unions, and errors with Effect's equality and hashing support; ordinary objects and arrays already compare structurally in Effect 4.
Creating values with structural equality
// run: Console.log(Equal.equals(first, second), Equal.equals(["inbox", 5] as const, ["inbox", 5] as const), Equal.equals([2, 4, 6], [2, 4, 6]), Equal.equals(makeRoute("inbox"), makeRoute("inbox")))
const first = { route: "inbox", filters: { unread: true } }
const second = { route: "inbox", filters: { unread: true } }
Equal.equals(first, second) // true
Equal.equals(["inbox", 5] as const, ["inbox", 5] as const) // true
Equal.equals([2, 4, 6], [2, 4, 6]) // true
function makeRoute(name: string) {
return { _tag: "Route" as const, name }
}
Equal.equals(makeRoute("inbox"), makeRoute("inbox")) // truetrue true true truePlain records, tuples, arrays, maps, and sets need no Data wrapper for structural equality. Comparison descends into nested values. Treat compared data as immutable: equality and hashing cache information about objects.
Building ordinary and tagged classes
// run: Console.log(label.upper, Equal.equals(label, new Label({ text: "pending" })), new Archived({ documentId: "doc-3" })._tag)
class Label extends Data.Class<{ text: string }> {
get upper() { return this.text.toUpperCase() }
}
class Archived extends Data.TaggedClass("Archived")<{
documentId: string
}> {}
const label = new Label({ text: "pending" })
label.upper // "PENDING"
Equal.equals(label, new Label({ text: "pending" })) // true
new Archived({ documentId: "doc-3" })._tag // "Archived"PENDING true ArchivedClass supplies a field-object constructor plus equality and hashing. TaggedClass also supplies a fixed _tag, useful when narrowing union members. These concrete data classes are distinct from the abstract classes used to declare services.
Defining tagged unions with matching helpers
// run: Console.log(isSent(sent)); Console.log(describe(waiting)); Console.log(describe(sent)); Console.log(describe(rejected))
type Delivery = Data.TaggedEnum<{
Waiting: {}
Sent: { tracking: string }
Rejected: { reason: string }
}>
const Delivery = Data.taggedEnum<Delivery>()
const sent = Delivery.Sent({ tracking: "parcel-8" })
const waiting = Delivery.Waiting()
const rejected = Delivery.Rejected({ reason: "address missing" })
const isSent = Delivery.$is("Sent")
isSent(sent) // true
const describe = Delivery.$match({
Waiting: () => "Waiting for dispatch",
Sent: ({ tracking }) => `Tracking: ${tracking}`,
Rejected: ({ reason }) => `Rejected: ${reason}`
})
describe(sent) // "Tracking: parcel-8"true
Waiting for dispatch
Tracking: parcel-8
Rejected: address missingTaggedEnum describes the union from a mapping of tags to payloads. taggedEnum creates constructors that insert _tag, a $is guard factory, and a $match helper that requires a case for each variant. Matching callbacks here are synchronous value computations; run service work after narrowing in an ordinary function.
Adding generic union cases
// run: Console.log(JSON.stringify([loaded, failed, idle]))
type LoadState<A, E> = Data.TaggedEnum<{
Idle: {}
Ready: { value: A }
Failed: { error: E }
}>
interface LoadStateDefinition extends Data.TaggedEnum.WithGenerics<2> {
taggedEnum: LoadState<this["A"], this["B"]>
}
const LoadState = Data.taggedEnum<LoadStateDefinition>()
const loaded = LoadState.Ready({ value: ["one", "two"] })
const failed = LoadState.Failed({ error: "offline" })
const idle = LoadState.Idle()[{"value":["one","two"],"_tag":"Ready"},{"error":"offline","_tag":"Failed"},{"_tag":"Idle"}]WithGenerics<2> gives the definition two type parameters, addressed as A and B. The generated constructors infer their payload types at each call. Keep the generic definition in a type or interface; it is not a generic service class.
Defining ordinary and tagged errors
// run: Console.log(new InvalidDocument({ message: "invalid", line: 3 }).line); Console.log(JSON.stringify([documentOrDraft("doc-3"), documentOrDraft("")])); try { requireDocument("") } catch (error) { Console.log(error._tag) }
class InvalidDocument extends Data.Error<{
message: string
line: number
}> {}
error DocumentMissing {
id: string
}
function requireDocument(id: string) {
if (id === "") throw new DocumentMissing({ id })
return { id }
}
// { id: string }!DocumentMissing!
function documentOrDraft(id: string) {
try {
return requireDocument(id)
} catch (error) {
return { id: "draft" }
}
}3
[{"id":"doc-3"},{"id":"draft"}]
DocumentMissingData.Error creates a native error with additional fields. The global TaggedError is Effect's Data.TaggedError, with an automatically populated _tag; use throw to raise it as an expected failure. Use Schema.TaggedError when the error needs a schema for serialization.
Preserving native error causes
// run: Console.log(JSON.stringify(decodeDocument("{\"id\":3}"))); try { decodeDocument("{") } catch (error) { Console.log(error._tag, error.cause instanceof SyntaxError) }
error DocumentDecodeError {
cause: unknown
}
function decodeDocument(text: string) {
return try((text) => JSON.parse(text), (cause) => new DocumentDecodeError({ cause }), text)
}
// any!DocumentDecodeError!{"id":3}
DocumentDecodeError trueA cause field preserves the original exception using native error-cause support. It is separate from Effect's Cause, which describes an entire execution failure. Preserve the original error for diagnosis, and avoid adding document contents or credentials to diagnostic fields.
Coming from Effect
Raise a data error with throw new DocumentMissing({ id }). Generator yielding and Effect.fail are unnecessary for ordinary VibeLang error propagation.
The matching Effect page: effect.website/docs/v4/data-types/data