Skip to content

Primitives, Literals & Unions

The leaves of every schema are primitives. Effect ships schemas for each JavaScript primitive, for individual literal values, for common built-in classes (Date, URL, RegExp, …), and for the special top/bottom types. You combine these — and the structs in the next pages — into larger shapes.

import { Schema } from "effect"
// A single description reusing primitive and literal schemas.
const Account = Schema.Struct({
id: Schema.String,
balance: Schema.Number,
active: Schema.Boolean,
// A union of literals: only these three strings are accepted.
tier: Schema.Literals(["free", "pro", "enterprise"])
})
const account = Schema.decodeUnknownSync(Account)({
id: "acc_1",
balance: 100,
active: true,
tier: "pro"
})
console.log(account.tier) // => "pro"

Every schema on this page is non-transforming: its decoded Type equals its encoded Encoded, so decoding and encoding pass values through unchanged (they only validate). Schemas that convert between representations — NumberFromString, DateFromString, the *FromString family, Option/Map/Set codecs — live in transformations.

These are the schemas you reach for constantly. Each decodes from and encodes to the same primitive type.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String)("hi") // => "hi"
Schema.decodeUnknownSync(Schema.Number)(42) // => 42
Schema.decodeUnknownSync(Schema.Boolean)(true) // => true
Schema.decodeUnknownSync(Schema.BigInt)(10n) // => 10n

For “this value, or null/undefined”, combine a primitive with a dedicated combinator instead of writing the union by hand:

import { Schema } from "effect"
const Nickname = Schema.NullOr(Schema.String) // string | null
const Bio = Schema.UndefinedOr(Schema.String) // string | undefined
const Avatar = Schema.NullishOr(Schema.String) // string | null | undefined

For a closed set of string constants (a “string enum”), use Schema.Literals:

import { Schema } from "effect"
const Status = Schema.Literals(["active", "inactive", "pending"])
type Status = typeof Status.Type // "active" | "inactive" | "pending"

The rest of this page is the exhaustive catalogue: every built-in primitive, literal/enum/template-literal constructor, and union combinator, each with a short runnable example.

Each schema below decodes from and encodes to the matching JavaScript primitive.

Accepts any string.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String)("hello") // => "hello"

Accepts any number, including NaN, Infinity, and -Infinity. Use Finite or Int to exclude those.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number)(3.14) // => 3.14
Schema.decodeUnknownSync(Schema.Number)(NaN) // => NaN (accepted)

Accepts true or false.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Boolean)(false) // => false

Accepts any bigint. (The string codec BigIntFromString lives in transformations.)

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt)(123n) // => 123n

Accepts any symbol. For one specific symbol, use UniqueSymbol.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Symbol)(Symbol.for("x")) // => Symbol(x)

Accepts only null.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Null)(null) // => null

Accepts only undefined.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Undefined)(undefined) // => undefined

A schema whose Type is void. Useful for the return position of effects/RPCs that produce no meaningful value.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Void)(undefined) // => undefined

Accepts any non-null object (typed as object) — arrays and functions included, but not primitives. This is the schema for the TypeScript object keyword; for actual struct shapes use Schema.Struct (see structs and records).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.ObjectKeyword)({ a: 1 }) // => { a: 1 }
Schema.decodeUnknownSync(Schema.ObjectKeyword)([1, 2]) // => [1, 2]
// Schema.decodeUnknownSync(Schema.ObjectKeyword)("x") -> throws

A union of the value types usable as object keys: finite number, symbol, or string (Schema.Union([Schema.Finite, Schema.Symbol, Schema.String])).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.PropertyKey)("key") // => "key"
Schema.decodeUnknownSync(Schema.PropertyKey)(0) // => 0

These describe the extreme ends of the type lattice. Use Unknown/Any deliberately — they opt that position out of validation.

Accepts any value, typed as unknown. Validation defers to wherever the value is later narrowed.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Unknown)({ anything: true }) // => { anything: true }

Accepts any value, typed as any. Same runtime behaviour as Unknown but with TypeScript’s escape-hatch type — prefer Unknown.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Any)(42) // => 42

Accepts nothing — decoding always fails. Useful as the “impossible” position in conditional types and as a placeholder.

import { Schema } from "effect"
// Schema.decodeUnknownSync(Schema.Never)(1) -> throws (no value is valid)

Refined primitives (still non-transforming)

Section titled “Refined primitives (still non-transforming)”

These attach a built-in filter to a base primitive. They validate more strictly but do not change the type — decode/encode still pass the value through.

Schema.Number that rejects NaN, Infinity, and -Infinity.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Finite)(3.14) // => 3.14
// Schema.decodeUnknownSync(Schema.Finite)(Infinity) -> throws

Schema.Number that requires an integer (also rejects NaN/Infinity).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Int)(7) // => 7
// Schema.decodeUnknownSync(Schema.Int)(7.5) -> throws

Schema.String with at least one character.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.NonEmptyString)("a") // => "a"
// Schema.decodeUnknownSync(Schema.NonEmptyString)("") -> throws

Schema.String whose JavaScript .length is exactly 1. Note this counts UTF-16 code units, so some single visible glyphs (emoji, combined characters) do not qualify.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Char)("x") // => "x"
// Schema.decodeUnknownSync(Schema.Char)("xy") -> throws

These validate that a value is an instance of a well-known JavaScript class. The decoded Type is the class instance and decoding passes it through unchanged. (A default JSON serializer is attached for each — see the note per schema — but that only matters when you go through the JSON codec layer.)

Accepts any Date instance, including invalid dates like new Date("nope"). Use DateValid to reject those, or Schema.DateFromString (in transformations) to decode from an ISO string.

import { Schema } from "effect"
const d = new Date("2024-01-01")
Schema.decodeUnknownSync(Schema.Date)(d) // => Date 2024-01-01T00:00:00.000Z
// Schema.decodeUnknownSync(Schema.Date)("2024-01-01") -> throws (string, not a Date)

Schema.Date that additionally rejects invalid Date instances.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.DateValid)(new Date("2024-01-01")) // => Date ...
// Schema.decodeUnknownSync(Schema.DateValid)(new Date("nope")) -> throws

Accepts any RegExp instance. The JSON serializer encodes it as { source, flags }.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.RegExp)(/ab+c/i) // => /ab+c/i

Accepts any URL instance. The JSON serializer encodes it as a string. To decode a URL from a string, use Schema.URLFromString (transformations).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.URL)(new URL("https://effect.website")) // => URL ...

Accepts any Uint8Array. The JSON serializer encodes it as a Base64 string.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Uint8Array)(new Uint8Array([1, 2, 3])) // => Uint8Array(3) [1, 2, 3]

Accepts any File instance. The JSON serializer encodes it as { data, type, name, lastModified } with data Base64-encoded.

import { Schema } from "effect"
const file = new File(["hi"], "greeting.txt", { type: "text/plain" })
Schema.decodeUnknownSync(Schema.File)(file) // => File ...

Accepts any FormData instance. The JSON serializer encodes it as an array of [key, entry] pairs.

import { Schema } from "effect"
const form = new FormData()
form.append("name", "Ada")
Schema.decodeUnknownSync(Schema.FormData)(form) // => FormData ...

Accepts any URLSearchParams instance. The JSON serializer encodes it as a query string.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.URLSearchParams)(
new URLSearchParams("page=1&limit=20")
) // => URLSearchParams { 'page' => '1', 'limit' => '20' }

Build an instanceof schema for any class, including your own. Decoding and encoding pass the value through unchanged.

import { Schema } from "effect"
class Point {
constructor(readonly x: number, readonly y: number) {}
}
const PointSchema = Schema.instanceOf(Point)
Schema.decodeUnknownSync(PointSchema)(new Point(1, 2)) // => Point { x: 1, y: 2 }
// Schema.decodeUnknownSync(PointSchema)({ x: 1, y: 2 }) -> throws (not a Point)

Accepts any immutable JSON-compatible value: null, number, boolean, string, readonly arrays of Json, or readonly string-keyed records of Json.

import { Schema } from "effect"
const r = Schema.decodeUnknownOption(Schema.Json)({ key: [1, true, null] })
console.log(r._tag) // => "Some"

Like Json but the arrays and records in its type are mutable.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.MutableJson)([1, "two", false]) // => [1, "two", false]

Matches one exact value. Accepts a single string, number, bigint, boolean, or null.

import { Schema } from "effect"
const Yes = Schema.Literal("yes")
Schema.decodeUnknownSync(Yes)("yes") // => "yes"
// Schema.decodeUnknownSync(Yes)("no") -> throws
// Read the literal back off the schema:
Yes.literal // => "yes"

A union of literal values — the idiomatic way to model a closed set of constants. The members are tried in order; .pick narrows to a subset.

import { Schema } from "effect"
const Status = Schema.Literals(["active", "inactive", "pending"])
type Status = typeof Status.Type // "active" | "inactive" | "pending"
Schema.decodeUnknownSync(Status)("active") // => "active"
// Narrow to a subset, preserving the literal type:
const Live = Status.pick(["active", "pending"])
type Live = typeof Live.Type // "active" | "pending"
// Access the underlying literal array and member schemas:
Status.literals // => ["active", "inactive", "pending"]

Builds a schema from a TypeScript enum, accepting any of its values.

import { Schema } from "effect"
enum Direction {
Up = "UP",
Down = "DOWN"
}
const DirectionSchema = Schema.Enum(Direction)
Schema.decodeUnknownSync(DirectionSchema)(Direction.Up) // => "UP"
Schema.decodeUnknownSync(DirectionSchema)("DOWN") // => "DOWN"
// Schema.decodeUnknownSync(DirectionSchema)("LEFT") -> throws
DirectionSchema.enums // => the original enum object

Matches one specific symbol (and only that symbol). For any symbol, use Symbol.

import { Schema } from "effect"
const Brand = Symbol.for("Brand")
const BrandSchema = Schema.UniqueSymbol(Brand)
Schema.decodeUnknownSync(BrandSchema)(Brand) // => Symbol(Brand)
// Schema.decodeUnknownSync(BrandSchema)(Symbol.for("Other")) -> throws

Validates strings matching a template-literal pattern. Each part is either a literal string/number/bigint or a schema whose encoded type is a string, number, or bigint. The value stays a string — this only validates the shape.

import { Schema } from "effect"
const Route = Schema.TemplateLiteral(["/user/", Schema.Number])
// Type: `/user/${number}`
Schema.decodeUnknownSync(Route)("/user/123") // => "/user/123"
// Schema.decodeUnknownSync(Route)("/user/abc") -> throws
Route.parts // => ["/user/", Schema.Number]

Same matching as TemplateLiteral, but decodes the matched parts into a readonly tuple (one element per part). Literal parts contribute their literal value; schema parts contribute their decoded Type — so use a decoding schema like Schema.NumberFromString to actually convert a captured segment.

import { Schema } from "effect"
const Route = Schema.TemplateLiteralParser(["/user/", Schema.NumberFromString])
// Decode: string -> typed tuple
Schema.decodeUnknownSync(Route)("/user/42") // => ["/user/", 42]
// Encode: tuple -> string
Schema.encodeSync(Route)(["/user/", 42]) // => "/user/42"

Accepts a value matching any member. By default (mode: "anyOf") members are tried in order and the first match wins; pass mode: "oneOf" to require that exactly one member matches.

import { Schema } from "effect"
const StringOrNumber = Schema.Union([Schema.String, Schema.Number])
Schema.decodeUnknownSync(StringOrNumber)("hello") // => "hello"
Schema.decodeUnknownSync(StringOrNumber)(42) // => 42
// Access / transform members:
StringOrNumber.members // => [Schema.String, Schema.Number]

Schema.Union([schema, Schema.Null]) — the value or null.

import { Schema } from "effect"
const Name = Schema.NullOr(Schema.String) // string | null
Schema.decodeUnknownSync(Name)(null) // => null
Schema.decodeUnknownSync(Name)("Ada") // => "Ada"

Schema.Union([schema, Schema.Undefined]) — the value or undefined.

import { Schema } from "effect"
const Bio = Schema.UndefinedOr(Schema.String) // string | undefined
Schema.decodeUnknownSync(Bio)(undefined) // => undefined

Schema.Union([schema, Schema.Null, Schema.Undefined]) — the value, null, or undefined.

import { Schema } from "effect"
const Avatar = Schema.NullishOr(Schema.String) // string | null | undefined
Schema.decodeUnknownSync(Avatar)(null) // => null
Schema.decodeUnknownSync(Avatar)(undefined) // => undefined

The OptionFrom* combinators (decoding null/undefined/missing into an Option) are codecs, so they live in transformations. For discriminated unions keyed by a _tag field, use Schema.TaggedUnion, covered in structs and records.

When none of the built-ins fit, Schema.declare builds an opaque schema from a type-guard. The schema accepts any value and succeeds when the guard returns true. It’s the lowest-level primitive constructor — instanceOf is built on top of it.

import { Schema } from "effect"
type UserId = string & { readonly _tag: "UserId" }
const isUserId = (u: unknown): u is UserId =>
typeof u === "string" && u.startsWith("user_")
const UserId = Schema.declare<UserId>(isUserId, {
title: "UserId",
description: "A user identifier starting with 'user_'"
})
Schema.decodeUnknownSync(UserId)("user_42") // => "user_42"
// Schema.decodeUnknownSync(UserId)("nope") -> throws

For parametric types — Option<A>, a custom Box<A>, etc. — use Schema.declareConstructor, which threads the inner type parameters’ schemas through. This is an advanced escape hatch; reach for it only when a type has type parameters that must themselves be validated.

  • Decode primitives from strings (NumberFromString, DateFromString, the *FromString family) and convert to Option/Map/Set: transformations.
  • Tighten a primitive with custom validations: filters.
  • Assemble primitives into objects and collections: structs and records.