API reference / @evolu/common / Type / LiteralType
Interface: LiteralType<T>
Defined in: packages/common/src/Type.ts:2263
Evolu Type is like a type guard that returns typed errors (via Result) instead of throwing. We either get a safely typed value or a composable typed error telling us exactly why validation failed.
Why another validation library?
- Result-based error handling – no exceptions for normal control flow.
- Typed errors with decoupled formatters – validation logic ≠ user messages.
- Consistent constraints via Brand – every constraint becomes part of the type.
- Skippable validation – parent validations can be skipped when already proved by types.
- Simple, top-down implementation – readable source code from top to bottom.
- No user-land chaining DSL – prepared for TC39 Hack pipes.
A distinctive feature of Evolu Type compared to other validation libraries is that it returns typed errors rather than string messages. This allows TypeScript to enforce that all validation errors are handled by type checking, significantly improving the developer experience.
Evolu Type supports Standard Schema for interoperability with 40+ validation-compatible tools and frameworks.
Base Types Quick Start
// Validate unknown values
const value: unknown = "hello";
const stringResult = String.fromUnknown(value);
if (!stringResult.ok) {
// console.error(formatStringError(stringResult.error));
return stringResult; // inside a function returning Result<string, _>
}
// Safe branch: value is now string
const upper = stringResult.value.toUpperCase();
// Type guard style
if (String.is(value)) {
// narrowed to string
}
// Composing: arrays & objects
const Numbers = array(Number); // ReadonlyArray<number>
const Point = object({ x: Number, y: Number });
Numbers.from([1, 2, 3]); // ok
Point.from({ x: 1, y: 2 }); // ok
Point.from({ x: 1, y: "2" }); // err -> nested Number error
Branding Basics
Branding adds semantic meaning & constraints while preserving the runtime shape:
const CurrencyCode = brand("CurrencyCode", String, (value) =>
/^[A-Z]{3}$/.test(value)
? ok(value)
: err<CurrencyCodeError>({ type: "CurrencyCode", value }),
);
type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
const formatCurrencyCodeError = createTypeErrorFormatter<CurrencyCodeError>(
(error) => `Invalid currency code: ${error.value}`,
);
const r = CurrencyCode.from("USD"); // ok("USD")
const e = CurrencyCode.from("usd"); // err(...)
See also reusable brand factories like minLength, maxLength, trimmed,
positive, between, etc.
Objects & Optional Fields
const User = object({
name: NonEmptyTrimmedString100,
age: optional(PositiveInt),
});
type User = typeof User.Type;
User.from({ name: "Alice" }); // ok
User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
Deriving JSON String Types
const Person = object({
name: NonEmptyString50,
// Did you know that JSON.stringify converts NaN (a number) into null?
// To prevent this, use FiniteNumber.
age: FiniteNumber,
});
type Person = typeof Person.Type;
const [PersonJson, personToPersonJson, personJsonToPerson] = json(
Person,
"PersonJson",
);
// string & Brand<"PersonJson">
type PersonJson = typeof PersonJson.Type;
const person = Person.orThrow({
name: "Alice",
age: 30,
});
const personJson = personToPersonJson(person);
expect(personJsonToPerson(personJson)).toEqual(person);
Error Formatting
Evolu separates validation logic from human-readable messages. There are two layers:
- Per-type formatters (e.g.
formatStringError) – simple, focused, already used earlier in the quick start example. - A unified formatter via
createFormatTypeError– composes all built-in and custom errors (including nested composite types) and lets us override selected messages.
1. Per-Type Formatter (recap)
const r = String.fromUnknown(42);
if (!r.ok) console.error(formatStringError(r.error));
2. Unified Formatter with Overrides
// Override only what we care about; fall back to built-ins for the rest.
const formatTypeError = createFormatTypeError((error) => {
if (error.type === "MinLength") return `Min length is ${error.min}`;
});
const User = object({ name: NonEmptyTrimmedString100 });
const resultUser = User.from({ name: "" });
if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
const badPoint = object({ x: Number, y: Number }).from({
x: 1,
y: "foo",
});
if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
The unified formatter walks nested structures (object / array / record / tuple / union) and applies overrides only where specified, greatly reducing boilerplate when formatting complex validation errors.
Tip
If necessary, write globalThis.String instead of String to avoid naming
clashes with native types.
Design Decision: No Bidirectional Transformations
Evolu Type intentionally does not support bidirectional transformations. It previously did, but supporting that while keeping typed error fidelity added complexity that hurt readability & reliability. Most persistence pipelines (e.g. SQLite) already require explicit mapping of query results, so implicit reverse transforms would not buy much. We may revisit this if we can design a minimal, 100% safe API that preserves simplicity.
Prepared for TC39 Hack Pipes
Take a look how SimplePassword is defined:
export const SimplePassword = brand(
"SimplePassword",
minLength(8)(maxLength(64)(TrimmedString)),
);
Nested functions are often OK (if not, make a helper) and read well, but with TC39 Hack pipes it would be clearer:
// TrimmedString
// |> minLength(8)(%)
// |> maxLength(64)(%)
// |> brand("SimplePassword", %)
Note minLength and maxLength are curried because they are factories.
Extends
Type<"Literal",T,WidenLiteral<T>,LiteralError<T>>
Type Parameters
| Type Parameter |
|---|
T extends Literal |
Properties
| Property | Modifier | Type | Description | Inherited from | Defined in |
|---|---|---|---|---|---|
[EvoluTypeSymbol] | readonly | true | - | Type.[EvoluTypeSymbol] | packages/common/src/Type.ts:353 |
~standard | readonly | Props<WidenLiteral<T>, T> | The Standard Schema properties. | Type.~standard | packages/common/src/Type.ts:4450 |
Error | public | LiteralError<T> | The specific error introduced by this Type. ### Example type StringError = typeof String.Error; | Type.Error | packages/common/src/Type.ts:221 |
Errors | readonly | LiteralError<T> | ### Example type StringParentErrors = typeof String.Errors; | Type.Errors | packages/common/src/Type.ts:417 |
expected | public | T | - | - | packages/common/src/Type.ts:2269 |
from | readonly | (value) => Result<T, LiteralError<T>> | Creates T from an Input value. This is useful when we have a typed value. from is a typed alias of fromUnknown. | Type.from | packages/common/src/Type.ts:236 |
fromParent | readonly | (value) => Result<T, LiteralError<T>> | Creates T from Parent type. This function skips parent Types validations when we have already partially validated value. | Type.fromParent | packages/common/src/Type.ts:330 |
fromUnknown | readonly | (value) => Result<T, LiteralError<T>> | Creates T from an unknown value. This is useful when a value is unknown. | Type.fromUnknown | packages/common/src/Type.ts:322 |
Input | public | WidenLiteral<T> | The type expected by from and fromUnknown. ### Example type StringInput = typeof String.Input; | Type.Input | packages/common/src/Type.ts:219 |
is | readonly | Refinement<unknown, T> | A type guard that checks whether an unknown value satisfies the Type. ### Example const value: unknown = "hello"; if (String.is(value)) { // TypeScript now knows valueis astring here. console.log("This is a valid string!"); } const strings: unknown[] = [1, "hello", true, "world"]; const filteredStrings = strings.filter(String.is); console.log(filteredStrings); // ["hello", "world"] | Type.is | packages/common/src/Type.ts:351 |
name | readonly | "Literal" | - | Type.name | packages/common/src/Type.ts:227 |
orNull | readonly | (value) => T | null | Creates T from an Input value, returning null if validation fails. This is a convenience method that combines from with getOrNull. When to use: - When you need to convert a validation result to a nullable value - When the error is not important and you just want the value or nothing ### Example // ✅ Good: Optional user input const age = PositiveInt.orNull(userInput); if (age != null) { console.log("Valid age:", age); } // ✅ Good: Default fallback const maxRetries = PositiveInt.orNull(config.retries) ?? 3; // ❌ Avoid: When you need to know why validation failed (use from instead) const result = PositiveInt.from(userInput); if (!result.ok) { console.error(formatPositiveError(result.error)); } | Type.orNull | packages/common/src/Type.ts:315 |
orThrow | readonly | (value) => T | Creates T from an Input value, throwing an error if validation fails. Throws an Error with the Type validation error in its cause property, making it debuggable while avoiding the need for custom error messages. This is a convenience method that combines from with getOrThrow. When to use: - Configuration values that are guaranteed to be valid (e.g., hardcoded constants) - Application startup where failure should crash the program - As an alternative to assertions when the Type error in the thrown Error's cause provides sufficient debugging information - Test code with known valid inputs (when error message clarity is not critical; for better test error messages, use Vitest schemaMatching + assert with .is()) ### Example // ✅ Good: Known valid constant const maxRetries = PositiveInt.orThrow(3); // ✅ Good: App configuration that should crash on invalid values const appName = SimpleName.orThrow("MyApp"); // ✅ Good: Instead of assert when Type error is clear enough // Context makes it obvious: count increments from non-negative value const currentCount = counts.get(id) ?? 0; const newCount = PositiveInt.orThrow(currentCount + 1); // ✅ Good: Test setup with known valid values const testUser = User.orThrow({ name: "Alice", age: 30 }); // ❌ Avoid: User input (use from instead) const userAge = PositiveInt.orThrow(userInput); // Could crash! // ✅ Better: Handle user input gracefully const ageResult = PositiveInt.from(userInput); if (!ageResult.ok) { // Handle validation error } | Type.orThrow | packages/common/src/Type.ts:284 |
Parent | public | T | The parent type. ### Example type StringParent = typeof String.Parent; | Type.Parent | packages/common/src/Type.ts:223 |
ParentError | public | LiteralError<T> | The parent's error. ### Example type StringParentError = typeof String.ParentError; | Type.ParentError | packages/common/src/Type.ts:225 |
Type | readonly | T | The type this Type resolves to. ### Example type String = typeof String.Type; | Type.Type | packages/common/src/Type.ts:364 |