Skip to content
LogoLogo

Data

Sources: VIP-15, VIP-16, VIP-28.

schema declarations

Syntax

SchemaDeclaration, which begins with SchemaStart, with Fields and Separators shared with error and data. schema and data are declaration keywords under the one rule of Declaration keywords, as type is in TypeScript: the keyword and the name on one line, where a declaration can start; anything else, such as const ok = schema satisfies Schema.Top, is ordinary TypeScript. A field type is parsed greedily as in a TypeScript type member: a line break separates members only after the type is complete. Thus role: followed by | "admin" on the next line continues the type. Fields use TypeScript's type-member separators, so { a: string b: number } is a syntax error, EK1504. schema error { … } and schema error = T name a schema error; schema error Name declares a schema error (Failures).

A schema declaration requires a field body or = type; a data declaration requires a variant body, even if empty. Unlike error, neither has a bodyless form. A recognized ordinary schema header missing its required declaration form is EK1503; a recognized data header missing its variant body is EK1604. These diagnostics do not turn a missing body into an empty one, reject an admitted schema alias, or remove the bodyless error and schema error forms. So schema as T;, schema satisfies T;, data as T; and data satisfies T; are syntax errors at statement start, and schema as = Schema.String and data as {} declare names as.

The incomplete forms below have no runtime counterpart. The repairs retain the existing declaration meanings, including empty bodies, schema aliases and bodyless errors:

VibeLang
// compiler-gap: pending VIP-9, VIP-15, VIP-16
service MissingService // EK0906
schema MissingSchema // EK1503
data MissingData // EK1604
declare service AmbientService // EK0907, not an additional EK0906
 
service RuntimeService { static readonly key = "examples/RuntimeService" }
schema EmptySchema {}
schema SchemaAlias = Schema.String
data NoCases {}
error Allowed
schema error AlsoAllowed
Effect
import { Context, Data, Effect, Exit, Layer, Schema } from "effect"
 
type RuntimeShape = {}
class RuntimeService extends Context.Service<RuntimeService, RuntimeShape>()("examples/RuntimeService") {}
const EmptySchema = Schema.Struct({})
const SchemaAlias = Schema.String
type NoCases = Data.TaggedEnum<{}>
const NoCases = Data.taggedEnum<NoCases>()
class Allowed extends Data.TaggedError("Allowed") {}
class AlsoAllowed extends Schema.TaggedError<AlsoAllowed>()("AlsoAllowed", {}) {}
const declarationRepairExit = Effect.runSyncExit(Effect.provide(Effect.gen(function* () {
  const service = yield* RuntimeService
  const empty = yield* Schema.decodeUnknownEffect(EmptySchema)({})
  const alias = yield* Schema.decodeUnknownEffect(SchemaAlias)("ok")
  return [
    Object.keys(service).length, Object.keys(empty).length, alias,
    new Allowed()._tag, new AlsoAllowed()._tag, Object.keys(NoCases).length
  ]
}), Layer.succeed(RuntimeService, {})))
console.log(declarationRepairExit) // Success([0, 0, "ok", "Allowed", "AlsoAllowed", 0])

Meaning

schema Name { fields } and schema Name = type declare a Schema constant and its type under one name. Mappings preserve TypeScript mutability: properties are writable unless readonly; arrays and tuples are mutable unless readonly.

VibeLang
// compiler-gap: pending VIP-15
schema Address
{ city: string }
schema User {
  id: string; nickname?: string;
  role:
    | "admin"
    | "member";
  home: Address;
}
schema Code
  = "root_missing" | "read_failed"
Effect
// fragment
const Address = Schema.Struct({ city: Schema.mutableKey(Schema.String) })
type Address = typeof Address.Type
const User = Schema.Struct({
  id: Schema.mutableKey(Schema.String),
  nickname: Schema.mutableKey(Schema.optional(Schema.String)),
  role: Schema.mutableKey(Schema.Literals(["admin", "member"])),
  home: Schema.mutableKey(Address),
})
type User = typeof User.Type
const Code = Schema.Literals(["root_missing", "read_failed"])
type Code = typeof Code.Type

The optional rows below show the default with exactOptionalPropertyTypes off; the optional-wrapper rule after the table applies when it is on.

In this table, S, A and B are the recursively mapped schemas of the corresponding type syntax. A reference names a Schema value, not an expanded TypeScript type alias, interface, enum or class. The generic built-ins Array<T>, ReadonlyArray<T>, Record<K, V> and Readonly<T> in the table mean the corresponding TypeScript-library type bindings, selected by ordinary type lookup and alias resolution, not by spelling. A local or imported type binding can shadow a library type; a value-only binding does not by itself replace the separate library type binding. When the resolved binding is not a mapped built-in, apply the existing Schema-value lookup and arity rules. A reference with no suitable Schema value is EK2109: declare a schema or name a Schema value. This does not expand arbitrary user type aliases or introduce generic Schema declarations. The generic rows require these resolved library bindings; T[] and readonly T[] are syntax mappings independent of a written Array name.

Type or fieldEffect 4.1.1 schema
(T)the mapping of T; parentheses only group
string, number, boolean, bigint, symbolSchema.String, Schema.Number, Schema.Boolean, Schema.BigInt, Schema.Symbol
single literal "a"Schema.Literal("a") (likewise numeric, boolean and bigint literals)
union consisting entirely of literals "a" | "b"Schema.Literals(["a", "b"])
other union A | BSchema.Union([A, B]), each member mapped separately in source order
null, undefinedSchema.Null, Schema.Undefined
identifier or qualified name resolving to a Schema value (Address, NonEmptyString, Schema.String)that value, emitted as written
T[], Array<T>Schema.mutable(Schema.Array(S))
readonly T[], ReadonlyArray<T>Schema.Array(S)
[A, B]Schema.mutable(Schema.Tuple([A, B]))
readonly [A, B]Schema.Tuple([A, B])
[A, B?]Schema.mutable(Schema.Tuple([A, Schema.optional(B)]))
[A, ...B[]]Schema.mutable(Schema.TupleWithRest(Schema.Tuple([A]), [B]))
Record<K, V>Schema.Record(K, Schema.mutableKey(V))
Readonly<Record<K, V>>Schema.Record(K, V)
nested object { name: T }Schema.Struct({ name: Schema.mutableKey(S) })
string, numeric or computed field keythe field property key; computed names use the runtime schema-object rules below
writable field name: Tname: Schema.mutableKey(S)
writable optional field name?: Tname: Schema.mutableKey(Schema.optional(S))
readonly field readonly name: Tname: S
readonly optional field readonly name?: Tname: Schema.optional(S)

Parenthesized unions within a union are flattened in source order, as in TypeScript: ("a" | "b") | null maps as "a" | "b" | null. In ("a" | "b")[], the element maps to Schema.Literals(["a", "b"]).

For example, "a" | "b" | null maps to Schema.Union([Schema.Literal("a"), Schema.Literal("b"), Schema.Null]); it does not group the string literals into Schema.Literals.

With exactOptionalPropertyTypes off, optional tuple elements use Schema.optional, as optional fields do: [string, number?] admits ["a", undefined], matching TypeScript’s default.

The project's exactOptionalPropertyTypes setting controls mapped optional fields and tuple elements, matching TypeScript under that same configuration:

Source optional field or tuple elementSetting off (default)Setting on
name?: T / T?Schema.optional(S): missing or present undefinedSchema.optionalKey(S): missing only
name?: T | undefined / (T | undefined)?Schema.optional(S)Schema.optional(S)

The rule applies to ordinary schemas, schema errors, and nested mappings; Schema.mutableKey keeps its current position outside the optional wrapper. Native data payloads follow TypeScript's optional-property types directly. Declarations are identical to the Effect TypeScript twin under the same setting. Without exactOptionalPropertyTypes, TypeScript has no exact optional key, so neither does the sugar: a program needing Schema.optionalKey writes Effect's class or schema directly. This is not a gap to fill.

Tuple element labels ([name: A, count?: B]) do not change the mapping. Optional and rest tuples omit Schema.mutable when readonly; rest elements map recursively, with any trailing required elements passed after the rest schema to Schema.TupleWithRest.

VibeLang
// compiler-gap: pending VIP-15
schema TupleForms {
  token: symbol;
  optional: [name: string, count?: number];
  readonly rest: readonly [string, ...number[]];
}
Effect
// fragment
const TupleForms = Schema.Struct({
  token: Schema.mutableKey(Schema.Symbol),
  optional: Schema.mutableKey(Schema.mutable(Schema.Tuple([
    Schema.String, Schema.optional(Schema.Number),
  ]))),
  rest: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]),
})
type TupleForms = typeof TupleForms.Type
const tupleForms = Schema.decodeUnknownSync(TupleForms)({
  token: Symbol.for("token"), optional: ["name", undefined], rest: ["name", 1, 2],
})
tupleForms.optional.push(3)

Schema.optional accepts a missing key or undefined, matching TypeScript’s default; with exactOptionalPropertyTypes, an optional key whose type excludes undefined instead uses Schema.optionalKey. Schema.mutableKey changes the property, not its nested value; Schema.mutable changes an array or tuple, not its elements. Record keys must satisfy Effect’s Schema.Record.Key constraint. Computed schema field names use native TypeScript runtime object-literal grammar and property-key type checks. A key call, parenthesized key or element-access key is admitted when that runtime object counterpart is admitted; type-member restrictions do not apply merely because the field's value is written as a type. Evaluate each admitted key once at the declaration's existing execution point in native property evaluation order, preserving getters, coercion, TDZ, thrown values and source-context throw checks. This follows ECMA-262's property definition evaluation and property-key conversion. The key expression is not a schema type reference. Infer the public type from the actual Schema value rather than a fabricated type literal containing the expression. A computed key of non-literal type gives an index-signature type, but the runtime struct checks only the key the expression evaluates to, exactly as in Effect TypeScript. With default decoding options, extra keys are ignored and stripped, as in Schema.Struct.

A noncomputed field named __proto__ denotes an own data field, including quoted or escaped names and fields in nested structs. Its generated schema-fields object uses ["__proto__"], preserving the field instead of invoking JavaScript object-literal prototype-setting semantics. Already-computed keys keep their existing evaluation order.

VibeLang
// compiler-gap: pending VIP-15
schema Sample {
  kind: "sample";
  value: string | null;
  absent: undefined;
  items: number[];
  readonly pair: readonly [string, number];
  counts: Record<string, number>;
  nested: { active: boolean };
}
Effect
// fragment
const Sample = Schema.Struct({
  kind: Schema.mutableKey(Schema.Literal("sample")),
  value: Schema.mutableKey(Schema.Union([Schema.String, Schema.Null])),
  absent: Schema.mutableKey(Schema.Undefined),
  items: Schema.mutableKey(Schema.mutable(Schema.Array(Schema.Number))),
  pair: Schema.Tuple([Schema.String, Schema.Number]),
  counts: Schema.mutableKey(Schema.Record(Schema.String, Schema.mutableKey(Schema.Number))),
  nested: Schema.mutableKey(Schema.Struct({ active: Schema.mutableKey(Schema.Boolean) })),
})
type Sample = typeof Sample.Type
const sample = Schema.decodeUnknownSync(Sample)({
  kind: "sample", value: null, absent: undefined, items: [1],
  pair: ["a", 2], counts: { a: 1 }, nested: { active: true },
})
sample.items.push(2)
sample.counts.a = 2

The numeric keys below use call, parenthesized and element-access expressions. All pass native runtime object-key checks; the schema error also satisfies the existing reserved-_tag check because its key domain is numeric.

VibeLang
// compiler-gap: pending VIP-15, VIP-29
let schemaKeyCalls = 0
function schemaKey(): number { schemaKeyCalls++; return 42 }
const Box = { get field(): number { schemaKeyCalls++; return 43 } }
schema RuntimeCounts { [schemaKey()]: number; [(Box.field)]: number }
schema error RuntimeCountError { [Box["field"]]: number }
Effect
import { Effect, Exit, Schema } from "effect"
 
let schemaKeyCalls = 0
function schemaKey(): number { schemaKeyCalls++; return 42 }
const Box = { get field(): number { schemaKeyCalls++; return 43 } }
const RuntimeCounts = Schema.Struct({
  [schemaKey()]: Schema.mutableKey(Schema.Number),
  [(Box.field)]: Schema.mutableKey(Schema.Number)
})
type RuntimeCounts = typeof RuntimeCounts.Type
class RuntimeCountError extends Schema.TaggedError<RuntimeCountError>()("RuntimeCountError", {
  [Box["field"]]: Schema.Number
}) {}
const runtimeKeysExit = Effect.runSyncExit(Effect.gen(function* () {
  const decoded = yield* Schema.decodeUnknownEffect(RuntimeCounts)({ 42: 1, 43: 2, 44: 99 })
  const missing = Schema.decodeUnknownExit(RuntimeCounts)({ 42: 1 })
  return [
    schemaKeyCalls, decoded[42], decoded[43], Object.hasOwn(decoded, "44"),
    Exit.isFailure(missing), new RuntimeCountError({ 43: 7 })[43]
  ]
}))
console.log(runtimeKeysExit) // Success([3, 1, 2, false, true, 7])

Referenced schemas must preserve the source's decoded-key modifiers. At every required field, required fixed tuple slot (before or after a rest), and Record value, the inferred schema type's "~type.optionality" must be "required". At every effectively readonly field or readonly Record value, "~type.mutability" must be "readonly"; all schema error payload fields are effectively readonly even without a written readonly. These checks apply at the mapped position, recursively in nested structs and schema errors, through aliases and unions. A conflicting or unprovable flag is EK2109: use an unmodified value schema and write modifiers on the declaration. Writable and optional source positions retain the mappings above. The checks describe decoded Type; encoded-key and constructor-input defaults keep Effect's behavior. Field readonlyness is shallow and does not freeze nested mutable values.

VibeLang
// compiler-gap: pending VIP-15, VIP-29
const OptionalNumber = Schema.optional(Schema.Number)
const MutableString = Schema.mutableKey(Schema.String)
// schema Bad { must: OptionalNumber; readonly frozen: MutableString; tuple: [OptionalNumber]; entries: Record<"a", OptionalNumber> }
// schema error BadError { value: MutableString }
// EK2109 at each conflicting reference: the written required/readonly position controls.
schema FlagIntent {
  must: number | undefined;
  readonly frozen: string;
  tuple: [number | undefined];
  entries: Record<"a", number | undefined>;
}
schema error FlagError { value: string }
Effect
import { Schema } from "effect"
 
const PresentNumber = Schema.Union([Schema.Number, Schema.Undefined])
const FlagIntent = Schema.Struct({
  must: Schema.mutableKey(PresentNumber),
  frozen: Schema.String,
  tuple: Schema.mutableKey(Schema.mutable(Schema.Tuple([PresentNumber]))),
  entries: Schema.mutableKey(Schema.Record(Schema.Literal("a"), Schema.mutableKey(PresentNumber))),
})
type FlagIntent = typeof FlagIntent.Type
class FlagError extends Schema.TaggedError<FlagError>()("FlagError", { value: Schema.String }) {}
const decodeFlagIntent = Schema.decodeUnknownExit(FlagIntent)
decodeFlagIntent({ must: undefined, frozen: "ok", tuple: [undefined], entries: { a: undefined } }) // Success
decodeFlagIntent({ frozen: "ok", tuple: [undefined], entries: { a: undefined } })                  // Failure: required field
decodeFlagIntent({ must: undefined, frozen: "ok", tuple: [], entries: { a: undefined } })          // Failure: required tuple slot
decodeFlagIntent({ must: undefined, frozen: "ok", tuple: [undefined], entries: {} })               // Failure: required record key

A schema's encoded representation follows its field codecs. JSON and other transports must support that representation; a declaration does not automatically convert unsupported values or property keys. For example, raw Schema.BigInt encodes a bigint, which JSON cannot serialize, while Schema.BigIntFromString encodes a string. Ordinary JSON omits object properties with undefined, symbol or function values, ignores symbol keys, and converts nonfinite numbers to null. Cycles and user serialization hooks retain their ordinary JSON behavior. Choose codecs and a transport that preserve the required data, including for schema errors.

VibeLang
// compiler-gap: pending VIP-15, VIP-29
schema error BigCount { count: bigint }
schema error JsonCount { count: Schema.BigIntFromString }
Effect
import { Effect, Schema } from "effect"
 
class BigCount extends Schema.TaggedError<BigCount>()("BigCount", { count: Schema.BigInt }) {}
class JsonCount extends Schema.TaggedError<JsonCount>()("JsonCount", { count: Schema.BigIntFromString }) {}
const encodedBigCount = Schema.encodeSync(BigCount)(new BigCount({ count: 1n }))
const jsonTransportExit = Effect.runSyncExit(Effect.try({
  try: () => JSON.stringify(encodedBigCount), catch: (error) => error,
})) // Failure(TypeError): the encoded count is still a bigint
const encodedJsonCount = Schema.encodeSync(JsonCount)(new JsonCount({ count: 1n }))
const jsonRoundTripExit = Schema.decodeUnknownExit(JsonCount)(JSON.parse(JSON.stringify(encodedJsonCount)))
// Success(JsonCount({ count: 1n })); the encoded count was "1"

Refinements and transformations stay Schema constants. In a schema type, an identifier or qualified name is looked up as a value and must satisfy Effect's Schema.Top; a type of the same name is not expanded. Imports and shadowing follow TypeScript value lookup. The generic built-in rows first identify the library type binding as specified above. typeof Name.Type is the declared type.

VibeLang
// compiler-gap: pending VIP-15
import { NonEmptyString } from "effect/Schema"
schema Profile { name: NonEmptyString; title: Schema.String }
// type Role = "admin" | "member"
// schema Invalid { role: Role } // EK2109: declare Role with schema
Effect
// fragment
import { NonEmptyString } from "effect/Schema"
const Profile = Schema.Struct({
  name: Schema.mutableKey(NonEmptyString),
  title: Schema.mutableKey(Schema.String),
})
type Profile = typeof Profile.Type
VibeLang
// compiler-gap: pending VIP-15
const key = Symbol("key")
schema Keys { "a-b": string; 1: number; [key]: boolean }
Effect
// fragment
const key = Symbol("key")
const Keys = Schema.Struct({
  "a-b": Schema.mutableKey(Schema.String),
  1: Schema.mutableKey(Schema.Number),
  [key]: Schema.mutableKey(Schema.Boolean),
})
type Keys = typeof Keys.Type
VibeLang
// compiler-gap: pending VIP-15
const field: string = "actual"
schema Dynamic { [field]: number }
Effect
// fragment
const field: string = "actual"
const Dynamic = Schema.Struct({ [field]: Schema.mutableKey(Schema.Number) })
type Dynamic = typeof Dynamic.Type
const admitted: Dynamic = { other: 1 } // index-signature type
const decoded = Schema.decodeUnknownExit(Dynamic)(admitted) // failure: missing actual
const kept = Schema.decodeUnknownSync(Dynamic)({ actual: 1, other: 2 }) // { actual: 1 }

Computed schema error field keys also obey the reserved-tag rule: reject a runtime key domain that cannot be proved to exclude "_tag", including generic, enum or branded string domains that may produce it. A nominal brand alone grants no exception. This restriction does not apply to ordinary schema struct keys.

The generated constants keep TypeScript declaration order and temporal dead zone: declare Address before a schema that reads it. There is no implicit reordering or Schema.suspend; a forward or cyclic value reference has the same initialization behavior as the shown TypeScript.

RuleDiagnostic
export default schema S { … } or export default schema S = T; declare the name and write export { S as default }EK1501
type parameters on a schema declaration; use an ordinary generic TypeScript factory returning a Schema valueEK1502
a recognized ordinary schema declaration missing its required body or admitted alias formEK1503
two fields on one line with no separator, { a: string b: number }; write ; or a line break between them (quick fix: insert ;)EK1504
a member of a schema body that is not a Field: schema S { m(): string }, schema S { [k: string]: number }EK1505
a schema declaration in an ambient source context, explicit or inherited, as for a service; it comes ahead of the same declaration’s EK1503EK1506
a field type outside the mapping (including a shadowing type without a suitable Schema value), or a referenced schema with conflicting or unprovable decoded-key flags at a required or effectively readonly position; use an unmodified value schema and write the modifiers on the declaration (for Date, use Schema.Date)EK2109

A schema error keeps the error-declaration diagnostic EK1305; it does not use EK1501. A schema's named default export and an ordinary generic factory are normal TypeScript:

Effect
import { Schema } from "effect"
const S = Schema.Struct({ value: Schema.mutableKey(Schema.Number) })
type S = typeof S.Type
export { S as default }
function schemaBox<A extends Schema.Constraint>(value: A) {
  return Schema.Struct({ value: Schema.mutableKey(value) })
}
const NumberBox = schemaBox(Schema.Number)
const namedSchemaExit = Schema.decodeUnknownExit(S)({ value: 1 }) // Success({ value: 1 })
const genericSchemaExit = Schema.decodeUnknownExit(NumberBox)({ value: 2 }) // Success({ value: 2 })

One written, nongeneric type X = typeof X.Type in the same scope replaces the synthesized alias of schema X, in either order, each declaration keeping its own JSDoc and export status. Without one, the same bare alias is synthesized and the declaration's JSDoc belongs to its constant only. A mapped generic built-in (Array<T>, ReadonlyArray<T>, Record<K, V>, Readonly<T>) is selected by its resolved TypeScript type binding, never its spelling. A record's value-key flags obey source modifiers at the mapped value, and a required fixed tuple slot can't inherit an optional flag from a schema reference.

data declarations

Syntax

DataDeclaration, which begins with DataStart, is a list of Variants, possibly empty.

data follows Declaration keywords, so return data as User[] and const data = … still work. Field and Separator are those of schema declarations. A missing separator between two variants or two fields of a variant, data D { A B } or A { x: number y: string }, is EK1605. Variant names are TypeScript IdentifierNames, including reserved words such as default. Variant field keys follow TypeScript's type-member checks. A broad entity-name key can produce an index signature; an arbitrary computed expression is not admitted merely because its type is a valid property-key type (TS1170 can apply); the resulting payload must still satisfy Effect’s Data.TaggedEnum constraint forbidding _tag. In particular, a string index signature includes _tag and fails that constraint; it is not a TS1170 error. Duplicate variant and field declarations receive the native TypeScript checks for their type members, rather than an unconditional duplicate-identifier diagnostic. Computed members with the same resolved name can merge when TypeScript admits them. Variants are separated as fields are, as the members of the Data.TaggedEnum type literal are, and a variant's fields may be readonly. A variant's { starts its field body even after line breaks; the LineTerminator alternative of Separator does not apply before that {.

Meaning

data Name { Variant; Variant { fields } } declares Effect's Data.TaggedEnum type and its Data.taggedEnum constructors under one name. Zero variants are allowed: data Empty {} maps to type Empty = Data.TaggedEnum<{}>; const Empty = Data.taggedEnum<Empty>().

VibeLang
// compiler-gap: pending VIP-16
data Delivery
{
  Waiting;
  Sent
 
  { tracking: string };
  Rejected { reason: string };
}
const tracking = "outer"
const d = Delivery.Sent({ tracking: "1Z" })
const label = match (d) { when { _tag: "Sent", let tracking }: tracking; default: "none"; }
Effect
// fragment
type Delivery = Data.TaggedEnum<{ Waiting: {}; Sent: { tracking: string }; Rejected: { reason: string } }>
const Delivery = Data.taggedEnum<Delivery>()
const tracking = "outer"
const d = Delivery.Sent({ tracking: "1Z" })
let label: string
if ((typeof d === "object" || typeof d === "function") && d !== null && "_tag" in d &&
    d._tag === "Sent" && "tracking" in d) {
  const tracking = d.tracking
  label = tracking
} else {
  label = "none"
}

$is and $match are reserved variant names in every data declaration, generic or not (EK1602): Effect's Data.taggedEnum uses those properties as helpers, so calling them does not construct the named variant. Rename the variant and its resolved references, for example to a fresh Is or Match. Other names retain the same constructors and nonthrowing contract.

VibeLang
// compiler-gap: pending VIP-16
// data Invalid { $is; $match; Normal } // EK1602 on each reserved name
// data InvalidGeneric<A> { $match { value: A } } // EK1602
data SafeNames { Is; Match; Normal }
Effect
import { Data, Effect } from "effect"
 
type SafeNames = Data.TaggedEnum<{ Is: {}; Match: {}; Normal: {} }>
const SafeNames = Data.taggedEnum<SafeNames>()
const safeNameExit = Effect.runSyncExit(Effect.sync(() => [SafeNames.Is(), SafeNames.Match(), SafeNames.Normal()]))
// Success([{ _tag: "Is" }, { _tag: "Match" }, { _tag: "Normal" }])

A data payload is emitted as a TypeScript type literal in Data.TaggedEnum. Its computed key expressions are type checked and erased: declaring or constructing data does not run those expressions. Ordinary value declarations keep their evaluation; a separate key initializer still runs where its statement stands. Schema field keys retain their documented runtime evaluation.

VibeLang
// compiler-gap: pending VIP-16
let keyReads = 0; let initializerRuns = 0
const Keys = { get field(): number { keyReads++; return 1 } }
const initializedKey = (++initializerRuns, 2)
data Reads { Item { [Keys.field]: string } }
data Initialized { Item { [initializedKey]: string } }
const read = Reads.Item({ 1: "read" })
const eager = Initialized.Item({ 2: "eager" })
effect function observeDataKeys() {
  return [keyReads, initializerRuns, read[1], eager[2], keyReads]
}
// data Calls { Item { [dynamicKey()]: string } } // TS1170 when dynamicKey returns number
Effect
import { Data, Effect, Exit } from "effect"
 
let keyReads = 0; let initializerRuns = 0
const Keys = { get field(): number { keyReads++; throw new Error("not evaluated") } }
const initializedKey = (++initializerRuns, 2)
type Reads = Data.TaggedEnum<{ Item: { [Keys.field]: string } }>
const Reads = Data.taggedEnum<Reads>()
type Initialized = Data.TaggedEnum<{ Item: { [initializedKey]: string } }>
const Initialized = Data.taggedEnum<Initialized>()
const read = Reads.Item({ 1: "read" })
const eager = Initialized.Item({ 2: "eager" })
function observeDataKeys() {
  return Effect.gen(function* () {
    return [keyReads, initializerRuns, read[1], eager[2], keyReads]
  })
}
const dataKeyExit = Effect.runSyncExit(observeDataKeys())
console.log(dataKeyExit) // Success([0, 1, "read", "eager", 0])

Computed payload members use TypeScript's own duplicate-member checks. The repeated computed field below is admitted with matching types; incompatible computed declarations instead receive the native TS2717 check.

VibeLang
// compiler-gap: pending VIP-16
const slot = "x" as const
data Repeated { Row { [slot]: number; ["x"]: number } }
effect function readRepeated() { return Repeated.Row({ x: 7 }).x }
Effect
import { Data, Effect } from "effect"
 
const slot = "x" as const
type Repeated = Data.TaggedEnum<{
  Row: { readonly [slot]: number; readonly ["x"]: number }
}>
const Repeated = Data.taggedEnum<Repeated>()
type IncompatibleComputed = {
  ["x"]: number
  // @ts-expect-error TS2717: subsequent computed members must have the same type
  ["x"]: string
}
const duplicateKeyExit = Effect.runSyncExit(Effect.sync(() => Repeated.Row({ x: 7 }).x))
console.log(duplicateKeyExit) // Success(7)

A generated variant constructor has the static contract @throws {never}, independently of the foreign-callable default in VIP-4. Calling Delivery.Sent({ tracking: "1Z" }) constructs the value directly; no Effect.try wrapper is needed. This adds no runtime validation or wrapping to Data.taggedEnum. Variant payload properties are readonly in the resulting type, even without a written readonly, as in Effect's Data.TaggedEnum.

The object-pattern test first checks for an ECMAScript object (including functions) and property presence, then reads the tag; see Pattern matching.

Variants are matched with TC39 object patterns on _tag (Pattern matching). A bare key such as { tracking } only tests that the property exists; binding it takes let tracking. Here label is "1Z"; replacing let tracking with tracking would read "outer". The variant constructors get no custom matchers.

VIP-7 includes data unions and tagged members in the exhaustiveness check (EK4701). Every normalized subject member must be one of the checked kinds in Exhaustiveness; another member turns the check off. Coverage is defined there too, and never changes the ordered tests that run. A class pattern naming a data declaration or variant is EK0702 (Class patterns).

RuleDiagnostic
a variant field named _tag; rename it, because the variant name supplies the tagEK1601
a variant named $is or $match, generic data included; rename it and its resolved references to a fresh nameEK1602
export default data D { … }; declare the name and write export { D as default }EK1603
a recognized data declaration missing its required variant bodyEK1604
two variants, or two fields of a variant, on one line with no separator, data D { A B } or A { x: number y: string }; write ; or a line break between them (quick fix: insert ;)EK1605
a malformed variant or a payload member that is not a Field: data D { A(x: number) }, data D { A { m(): void } }; write A { x: number }EK1606
a data declaration in an ambient source context, explicit or inherited, as for a service; it comes ahead of the same declaration’s EK1604EK1607

The named default-export counterpart is:

Effect
import { Data, Effect } from "effect"
type D = Data.TaggedEnum<{ A: {} }>
const D = Data.taggedEnum<D>()
export { D as default }
const namedDataExit = Effect.runSyncExit(Effect.sync(() => D.A())) // Success({ _tag: "A" })

Generic unions

A data declaration may have up to 4 type parameters, the limit of Effect's own WithGenerics. Parameters map by position to this["A"], this["B"], this["C"] and this["D"], whatever their source names. Constraints such as <A extends string> and defaults such as <A = string> are compile errors (EK2126 and EK2803). Write a default on a separate TypeScript type alias. TypeScript variance annotations in and out are preserved on the generated type alias and checked by TypeScript; const is rejected on the source parameter with TypeScript’s TS1277, as on a type alias. The helper for data Outcome<A, E> is named $Outcome$Definition, or $Outcome$Definition$2, $3, … when the module already declares that name, so it never collides with a user name and is the same on every build.

VibeLang
// compiler-gap: pending VIP-28
data Outcome<A, E> { Ok { value: A }; Err { error: E } }
Effect
// fragment
type Outcome<A, E> = Data.TaggedEnum<{ Ok: { value: A }; Err: { error: E } }>
interface $Outcome$Definition extends Data.TaggedEnum.WithGenerics<2> {
  readonly taggedEnum: Outcome<this["A"], this["B"]>
}
const Outcome = Data.taggedEnum<$Outcome$Definition>()
RuleDiagnostic
more than 4 type parameters; remove parameters beyond the limit of 4EK2801
a constraint on a type parameter; remove the constraintEK2126
a default on a type parameter; move it to a separate type aliasEK2803
VibeLang
// compiler-gap: pending VIP-28
// data Box<A = string> { Value { value: A } } // EK2803
data Box<A> { Value { value: A } }
type StringBox<A = string> = Box<A>
const boxed: StringBox = Box.Value({ value: "hello" })
Effect
// fragment
type Box<A> = Data.TaggedEnum<{ Value: { value: A } }>
interface $Box$Definition extends Data.TaggedEnum.WithGenerics<1> {
  readonly taggedEnum: Box<this["A"]>
}
const Box = Data.taggedEnum<$Box$Definition>()
type StringBox<A = string> = Box<A>
const boxed: StringBox = Box.Value({ value: "hello" })

Shadowing mapped built-ins

VibeLang
// fragment
// compiler-gap: pending VIP-15
export {}
type Record<K, V> = { k: K; v: V }
schema S { a: Record<string, number> } // EK2109: no Schema value for the local type

A repair supplies the actual decoder instead of silently selecting the library's dictionary mapping:

VibeLang
schema Entry { readonly k: string; readonly v: number }
schema S { readonly a: Entry }
Effect
import * as Schema from "effect/Schema"
 
const Entry = Schema.Struct({ k: Schema.String, v: Schema.Number })
type Entry = typeof Entry.Type
const S = Schema.Struct({ a: Entry })
type S = typeof S.Type

Declaration output

Exported schema and data declarations emit the TypeScript declarations of their Effect TypeScript counterparts above: a schema constant and its typeof Name.Type alias, or a Data.TaggedEnum alias and typed constructor constant. Generic data also emits the WithGenerics helper, named as above, when the exported constant references it; the helper is an implementation name, not a reserved source identifier. TypeScript declaration emission preserves inferred readonly payloads and the constructor signatures; it adds no runtime matcher.

A JSDoc comment on a schema declaration is emitted on its declare const only; its synthesized typeof Name.Type alias has no comment. This includes @internal, @public and @deprecated, as on the ordinary Effect TypeScript pair. Native removeComments and stripInternal behavior is unchanged.

VibeLang
import * as Schema from "effect/Schema"
 
/** A message on the wire. */
export schema Message { readonly id: string }
Effect
import * as Schema from "effect/Schema"
 
/** A message on the wire. */
export const Message = Schema.Struct({ id: Schema.String })
export type Message = typeof Message.Type

Without a written alias, declaration emit retains the comment on the constant and leaves the synthesized type alias bare. A written alias keeps its own JSDoc, order and export status under the written schema alias rule. For example:

VibeLang
import { Schema } from "effect";
/** The constant's documentation. */
export schema Message { id: string }
/** The type's documentation. */
export type Message = typeof Message.Type;
Effect
import { Schema } from "effect";
/** The constant's documentation. */
export const Message = Schema.Struct({ id: Schema.mutableKey(Schema.String) });
/** The type's documentation. */
export type Message = typeof Message.Type;

Exact optional keys

With strict and exactOptionalPropertyTypes enabled, the same optional-key contract is written as follows. Missing keys decode successfully; a present undefined fails for name and succeeds for explicit.

VibeLang
// compiler-gap: pending VIP-15
schema Exact {
  readonly name?: string
  readonly explicit?: string | undefined
}
Effect
import { Schema } from "effect"
const Exact = Schema.Struct({
  name: Schema.optionalKey(Schema.String),
  explicit: Schema.optional(Schema.Union([Schema.String, Schema.Undefined]))
})
type Exact = typeof Exact.Type