API reference › @evolu/common › Type › 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:
- 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
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
Type<"Optional",InferType<T>,InferInput<T>,InferError<T>,InferParent<T>,InferParentError<T>>
Properties
[EvoluTypeSymbol]
readonly [EvoluTypeSymbol]: true;
Defined in: packages/common/src/Type.ts:410
Inherited from
~standard
readonly ~standard: Props<InferInput<T>, InferType<T>>;
Defined in: packages/common/src/Type.ts:4878
The Standard Schema properties.
Inherited from
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
Errors
readonly Errors:
| InferError<T>
| InferParentError<T>;
Defined in: packages/common/src/Type.ts:474
Example
type StringParentErrors = typeof String.Errors;
Inherited from
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
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
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
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
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
name
readonly name: "Optional";
Defined in: packages/common/src/Type.ts:276
Inherited from
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
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
createRunadapters 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
causeprovides 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
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
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
readonly Type: InferType;
Defined in: packages/common/src/Type.ts:421
The type this Type resolves to.
Example
type String = typeof String.Type;