API reference@evolu/commonType › OptionalType

Defined in: packages/common/src/Type.ts:4362

Evolu Type is like a type guard that returns typed errors (via Result) instead of throwing. We either receive a safely typed value or a composable typed error that tells us exactly why the validation failed.

The reason why Evolu Type exists is that no other TypeScript validation library met Evolu's requirements. 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 via exhaustiveCheck, significantly improving the developer experience. Those requirements are:

  • 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.

Evolu Type supports Standard Schema for interoperability with 40+ validation-compatible tools and frameworks.

Base Types

// Validate unknown values
const value: unknown = "hello";
const stringResult = String.fromUnknown(value);
if (!stringResult.ok) {
  // console.error(formatStringError(stringResult.error));
  return stringResult;
}
// 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

Branded types

Branding is the recommended way to define types in Evolu. Instead of using primitive types like string or number directly, wrap them with brand to create semantically meaningful types. See Brand for why this matters.

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.

Object types

const User = object({
  name: NonEmptyTrimmedString100,
  age: optional(PositiveInt),
});

// Use interface for objects. TypeScript displays the interface name
// instead of expanding all properties.
interface User extends InferType<typeof User> {}

User.from({ name: "Alice" }); // ok
User.from({ name: "Alice", age: -1 }); // err(PositiveInt)

// TODO: Add `record`

JSON type

const Person = object({
  name: NonEmptyString50,
  // Did you know that JSON.stringify converts NaN (a number) into null?
  // To prevent this, use FiniteNumber.
  age: FiniteNumber,
});
interface Person extends InferType<typeof Person> {}

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:

  1. Per-type formatters (e.g. formatStringError) – simple, focused, already used earlier in the quick start example.
  2. 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

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.

Naming

Evolu Types intentionally use the same names as native JavaScript types (String, Number, Boolean, etc.). When you need to distinguish between an Evolu Type and the native type, use globalThis to reference the native one (e.g., globalThis.String, globalThis.Number).

Design decision

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.

Composition without pipe

Take a look how SimplePassword is defined:

const SimplePassword = brand(
  "SimplePassword",
  minLength(8)(maxLength(64)(TrimmedString)),
);

Shallow nesting often fits one line. If it doesn't, split into named parts:

const Min8TrimmedString64 = minLength(8)(maxLength(64)(TrimmedString));
const SimplePassword = brand("SimplePassword", Min8TrimmedString64);

FAQ

How do I create a generic interface like FooState<T>?

TypeScript's InferType extracts a concrete type, not a generic one. We cannot write interface FooState<T> extends InferType<typeof fooState<T>> because InferType needs a concrete Type instance.

The recommended approach is to define the generic interface manually, then create a Type factory that produces structurally compatible Types:

// Define the generic interface manually
interface FooState<T> {
  readonly value: T;
  readonly loading: boolean;
}

// Create a Type factory that produces Types matching the interface
const fooState = <T extends AnyType>(valueType: T) =>
  object({
    value: valueType,
    loading: Boolean,
  });

// Usage
const StringFooState = fooState(String);
type StringFooState = InferType<typeof StringFooState>;

// The interface and inferred type are structurally compatible
const state: FooState<string> = StringFooState.orThrow({
  value: "hi",
  loading: false,
});

This keeps the interface generic while having type-safe runtime validation for each concrete use.

Extends

Properties

[EvoluTypeSymbol]

readonly [EvoluTypeSymbol]: true;

Defined in: packages/common/src/Type.ts:410

Inherited from

Type.[EvoluTypeSymbol]


~standard

readonly ~standard: Props<InferInput<T>, InferType<T>>;

Defined in: packages/common/src/Type.ts:4878

The Standard Schema properties.

Inherited from

Type.~standard


Error

Error: InferError<T>;

Defined in: packages/common/src/Type.ts:270

The specific error introduced by this Type.

Example

type StringError = typeof String.Error;

Inherited from

Type.Error


Errors

readonly Errors:
  | InferError<T>
| InferParentError<T>;

Defined in: packages/common/src/Type.ts:474

Example

type StringParentErrors = typeof String.Errors;

Inherited from

Type.Errors


from

readonly from: (value: InferInput) => Result<InferType<T>,
  | InferError<T>
| InferParentError<T>>;

Defined in: packages/common/src/Type.ts:285

Creates T from an Input value.

This is useful when we have a typed value.

from is a typed alias of fromUnknown.

Inherited from

Type.from


fromParent

readonly fromParent: (value: InferParent) => Result<InferType<T>, InferError<T>>;

Defined in: packages/common/src/Type.ts:387

Creates T from Parent type.

This function skips parent Types validations when we have already partially validated value.

Inherited from

Type.fromParent


fromUnknown

readonly fromUnknown: (value: unknown) => Result<InferType<T>,
  | InferError<T>
| InferParentError<T>>;

Defined in: packages/common/src/Type.ts:379

Creates T from an unknown value.

This is useful when a value is unknown.

Inherited from

Type.fromUnknown


Input

Input: InferInput<T>;

Defined in: packages/common/src/Type.ts:268

The type expected by from and fromUnknown.

Example

type StringInput = typeof String.Input;

Inherited from

Type.Input


is

readonly is: Refinement<unknown, InferType<T>>;

Defined in: packages/common/src/Type.ts:408

A type guard that checks whether an unknown value satisfies the Type.

Example

const value: unknown = "hello";
if (String.is(value)) {
  // TypeScript now knows `value` is a `string` 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"]

Inherited from

Type.is


name

readonly name: "Optional";

Defined in: packages/common/src/Type.ts:276

Inherited from

Type.name


orNull

readonly orNull: (value: InferInput) =>
  | InferType<T>
  | null;

Defined in: packages/common/src/Type.ts:372

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));
}

Inherited from

Type.orNull


orThrow

readonly orThrow: (value: InferInput) => InferType;

Defined in: packages/common/src/Type.ts:341

Creates T from an Input value, throwing an error if validation fails.

Use this where failure should crash the current flow instead of being handled locally.

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:

  • Application startup or composition-root setup where errors must stop the program immediately. In Evolu apps, errors are handled by platform-specific createRun adapters at the app boundary.
  • Module-level constants
  • Test setup with values that are expected to be valid
  • As an alternative to assertions when the Type error in the thrown Error's cause provides sufficient debugging information

Prefer from in ordinary application logic where the caller can recover, show validation errors, or choose a different flow.

For clearer test failure messages on invalid input, 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 = Name.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
}

Inherited from

Type.orThrow


parent

readonly parent: T;

Defined in: packages/common/src/Type.ts:4370


Parent

Parent: InferParent<T>;

Defined in: packages/common/src/Type.ts:272

The parent type.

Example

type StringParent = typeof String.Parent;

Inherited from

Type.Parent


ParentError

ParentError: InferParentError<T>;

Defined in: packages/common/src/Type.ts:274

The parent's error.

Example

type StringParentError = typeof String.ParentError;

Inherited from

Type.ParentError


Type

readonly Type: InferType;

Defined in: packages/common/src/Type.ts:421

The type this Type resolves to.

Example

type String = typeof String.Type;

Inherited from

Type.Type