API reference › @evolu/common › Type › Type
Defined in: packages/common/src/Type.ts:405
A runtime representation of a TypeScript type with typed structured errors.
Evolu Type reports expected decoding failures through Result rather
than exceptions. It represents both an encoded Input and its semantic
Output, supporting validation, transformation, and canonical encoding.
Evolu Type is designed for correctness and developer experience. Correctness is especially important for local-first data: application authors cannot inspect or repair a user's data on a server because they do not have access to it. Type declarations must reject invalid data at system boundaries, then preserve those guarantees wherever the data travels.
To make correct code the easiest code to write, Evolu Type preserves as much information as TypeScript can express. Invalid declarations produce readable CompileTimeError types when the compiler can detect them, while runtime assertions enforce construction contracts it cannot prove. Together, these choices create a pit of success.
The implementation is optimized for minimal bundle size. Less descriptive assertion messages could make it even smaller, but Evolu keeps actionable messages as a deliberate tradeoff for developer experience.
The main properties of Evolu Type are:
- Result-based error handling – expected failures are explicit values.
- Typed errors with decoupled formatters – validation logic stays independent of user-facing messages, and errors can be handled exhaustively.
- Type-safe, tree-shakeable localization – formatter requirements are inferred from selected Types, while apps bundle exactly the locales they support so users can change language offline.
- Consistent constraints through Brand – every refinement constraint is represented in the TypeScript type, so an unconstrained parent value cannot be used where the constrained value is required.
- Typed inputs – prefer
fromand its.parententry points to connect precise producer and consumer contracts while preserving typed remaining errors; reservefromUnknownfor genuinely unknown values. - Lawful codecs – Types partially decode
InputtoOutputand totally encode every legitimateOutputtoCanonicalInput, the statically known subtype ofInputreturned by complete encoding. - A top-down implementation – the source is intended to be read from beginning to end.
Evolu Type supports Standard Schema for interoperability with compatible tools and frameworks while preserving each Type's exact Input and Output.
Evolu Type assumes that all executing code, including third-party dependencies, has been audited and is trusted. It validates data contracts under that assumption. Trusting code does not require trusting every value it returns, so uncertain values from legacy code or another realm can still be decoded at an explicit boundary. It does not protect against hostile executable behavior such as sabotaged Proxies or throwing traps; Type validation is not a security boundary for untrusted JavaScript.
Type declarations and their callbacks are trusted construction code. Evolu
Type leverages that trust for better developer experience and does not try to
recover from code that defeats the type system with any or casts, including
fabricating an Err for Result<_, never>. Runtime assertions still enforce
contracts TypeScript cannot express, and every Type declaration must be
tested for its expected successes and failures.
fromUnknown validates untyped input through the complete pipeline. from
and its .parent operations use their declared boundary to determine which
remaining stages can return validation errors, but assert that boundary at
runtime. orThrow and orNull reuse the deepest from operation accepting
Input, while to asserts its Output boundary. A failed assertion means
application code violated its static contract; it is a developer error, not
an expected validation failure. Its message identifies the expected Type and
its cause preserves the exact structured Output validation error.
Prefer the most precise typed boundary available. A value is not unknown
merely because it originated outside the application: forms, components, and
other producers often expose a string or a branded value that can connect
directly to a matching from boundary. Reserve fromUnknown for values
whose TypeScript type is genuinely unknown. is means exact membership in
the output domain, not merely that output-side parsing could succeed. A
successful fromUnknown result always satisfies is.
Decoding accepts a representation outside the Output domain only when the Type explicitly declares that representation, such as a transformation Input. Structural Types do not implicitly repair another JavaScript representation. In particular, array and tuple require dense own data elements, while the predefined Object, object, and record require plain objects with own enumerable data properties. They do not invoke accessors or materialize inherited and non-enumerable properties.
TypeScript object types are structural and do not encode JavaScript realm
identity. Structural Types therefore accept legitimate representations from
other realms. Prototype checks remain when the prototype defines the semantic
domain. Plain-object Types accept a null prototype or an immediate root
prototype whose own prototype is null; ordinary class instances and deeper
prototype chains are rejected. When Record decoding must construct a
normalized value, it uses a null prototype so every string key remains
ordinary data.
Evolu Type expects TypeScript's exactOptionalPropertyTypes compiler option
to be enabled.
Predefined Types intentionally use the names of corresponding JavaScript
built-ins because they represent those familiar value categories. If an
imported Type shadows a built-in in the same scope, access the built-in
through globalThis, JavaScript's standard cross-environment global object,
such as globalThis.String or globalThis.Date.
Example
import { String, type Result } from "@evolu/common";
const value: unknown = "hello";
const result = String.fromUnknown(value);
expectTypeOf(result).toEqualTypeOf<
Result<
string,
{
readonly type: "TypeOf";
readonly expected: "String";
readonly value: unknown;
}
>
>();
expectOk(result, "hello");
FAQ
What does a Type represent?
A Type is a lawful, pure codec for an exact semantic domain:
Input ── partial decode ──▶ Output
CanonicalInput ◀─── total encode ─── Output
CanonicalInput ⊆ Input
Read each line in the direction of its arrowhead. Input is the complete
typed decoding boundary, including candidates that validation can reject and
noncanonical representations that decoding can normalize. Output is the
validated semantic value. CanonicalInput is the statically known subtype of
Input returned by the complete to operation. It can be wider than the
values actually emitted when a refinement follows an arbitrary transformation
because TypeScript cannot determine which values its encoder returns for the
narrowed Output. fromUnknown and the from operations decode; to
encodes.
A lawful Type round-trips every Output:
fromUnknown(to(output)) ≈ ok(output)
Encoding can canonicalize a valid Input:
"0042" ──decode──▶ 42 ──encode──▶ "42"
Once canonicalized, repeating the decode-encode cycle must preserve that representation:
"42" ──decode──▶ 42 ──encode──▶ "42"
Here decode means running the complete decoding pipeline, as fromUnknown
does, and ≈ means equality appropriate for the semantic domain. Validation
refinements, Array Types, and
Object Types preserve these laws when their contained
Types do. A union additionally requires compatible dispatch: it
encodes through the first member matching the Output and decodes through the
first member accepting the Input. Member ordering is lawful only when those
choices agree semantically. Encoded representations can overlap even when
member Output types are disjoint.
When encoding returns a refined value unchanged, the refinement can narrow
CanonicalInput without changing its JavaScript representation. For example,
FiniteNumber has number as its Input, while its Output and
CanonicalInput are FiniteNumber: decoding can reject non-finite number
candidates, and encoding only receives validated finite Outputs. A
transformation can change the representation entirely. For
Int64FromInt64String, Input is string, Output is Int64, and
CanonicalInput is Int64String. Structural Type factories derive their
CanonicalInput recursively from their contained Types.
Why is to total?
Suppose a Type accepts only strings containing decimal digits and decodes
them to JavaScript numbers. Parsing "42" is possible, but the Type cannot
lawfully declare its Output as number:
digits-only string ──partial decode──▶ number
digits-only string ◀─── total encode ── number // impossible
number also contains negative and fractional numbers, NaN, positive and
negative infinity, and -0. None of those values has a digits-only
representation, so to could not encode every valid Output.
One lawful design narrows the Output to the exact representable domain:
digits-only string ──partial decode──▶ NonNegativeSafeInteger
digits-only string ◀─── total encode ── NonNegativeSafeInteger
"0042" ──decode──▶ 42 ──encode──▶ "42"
Another lawful design keeps number as the Output but expands the Input
representation to include a canonical string for every number, including
"NaN", "Infinity", "-Infinity", and "-0", as well as negative and
fractional numbers.
The same principle applies when converting between two representations. Give
each representation its own Type with the same exact Output. For example, a
string representation and a number representation can both decode to the
shared SafeInteger domain:
string ──partial decode──▶ SafeInteger
string ◀─── total encode ── SafeInteger
number ──partial decode──▶ SafeInteger
number ◀─── total encode ── SafeInteger
Conversion decodes the source representation, then total-encodes the shared Output into the target representation. If no lossless shared domain exists, the operation is a partial conversion, migration, or policy decision and should be an explicit function returning Result, not a Type transformation.
Why can a typed operation throw?
TypeScript proves structural assignability, but it cannot describe every runtime invariant. For example, it cannot express whether an object property is own, enumerable, or a data property. It also permits a wider object with excess properties where a narrower object type is expected.
fromUnknown treats such invalid external values as expected data and
returns a typed error. Typed boundaries instead assert the domain promised by
their parameter type. If application code claims an accessor-backed object or
an object with excess properties is an Object Output, the assertion throws
because the application contract is broken. orThrow and orNull preserve
the assertion at their typed Input boundary, then apply getOrThrow
or getOrNull only to validation failures returned by the remaining
pipeline.
Consequently, structural representation errors such as sparse Arrays, accessors, and excess properties normally do not enter user-facing validation in typed application flows. They violate the producer's declared contract and throw as developer errors. At a genuinely unknown boundary, such as a schema-authoring tool, import, or external protocol, the same issues are legitimate typed validation errors and their formatter messages are useful.
This distinction applies to data failures. Any Type operation, including
fromUnknown, can throw when trusted Type-declaration code, such as a
successful transformation callback, violates its declared contract.
Materialize accessor values into plain data, remove properties the Type does
not represent, or use a different Type. Silently discarding excess data would
make the code constructing it dead while appearing to encode it successfully.
One exact Object policy also keeps Output membership independent of parsing
configuration. Exact structural policies also keep Output membership
independent of whether a transformation happens to allocate a new value.
Evolu Type therefore does not invoke accessors, discard excess properties, or
make to fallible. This keeps to total for every legitimate Output and
lets transformations compose without an encoding-error channel.
How should values from another realm be handled?
Code trust and data validation are separate decisions. Values returned by trusted legacy code or another realm can still be uncertain and should be validated. Realm-neutral Types accept an otherwise legitimate representation without requiring conversion merely because its built-ins belong to another realm.
When an application trusts both the producer and its return contract, it can
cast the boundary API's unknown because validation is redundant. Use a
specialized Type or explicit transformation when the producer actually uses a
different representation that needs adaptation or normalization.
All executing JavaScript remains trusted. Deliberately forged built-ins, hostile Proxies, throwing traps, or sabotaged executable behavior can throw; Evolu Type does not selectively contain them or claim to be a security boundary for untrusted code.
Why doesn't Evolu Type extract data from rich objects?
Some validation libraries parse an object's data projection. An imaginary
validation library can enumerate own enumerable string properties and decode
them into a fresh plain object. That lets a class instance decode as plain
data while its prototype and methods are ignored. The same general policy can
treat a Date or Map as an empty Record and can invoke enumerable getters.
This is a coherent but intentionally forgiving normalization model.
Evolu Type validates exactly the runtime representation defined by each Type;
it does not implicitly project one representation into another. The
predefined Object defines an open plain-object representation with
unknown values, object defines a closed plain-object representation,
and record defines a plain-dictionary representation whose complete
set of own properties are its entries. Their realm-neutral plain-object rule
accepts a null prototype or an immediate root prototype whose own prototype
is null; ordinary class instances and deeper prototype chains are rejected.
Every property must be an enumerable data property; inherited members are not
entries, while accessors and hidden properties are invalid instead of being
invoked or ignored. array similarly defines a dense sequence whose
only own properties are length and its indexed data properties;
tuple applies the same representation rules with a fixed length and a
distinct Type for each position. Only an explicit transform changes
the representation. Consequently, is tests exact Output membership and to
stays total for valid Outputs.
Why is JsonValue stricter than JSON.stringify?
JSON.stringify is a forgiving data projection. It can invoke toJSON and
accessors, discard object properties, replace unsupported array elements and
non-finite numbers with null, and normalize -0 to 0. Those rules are
useful for ordinary serialization, but they do not preserve an exact value.
JsonValue instead defines data that is already represented as data.
Invalid runtime behavior and values are rejected rather than interpreted or
silently discarded. Its encoder is total and stack-safe for every valid
Output, and JsonValueFromJson preserves the semantic value when it is
encoded and decoded, including JavaScript's distinction between -0 and 0.
Use an explicit transformation before this boundary when a projection or
other normalization is desired.
Why are Types pure and synchronous?
A Type describes data meaning, not work. Time, I/O, dependencies, external state, authorization, and other contextual decisions belong in a Task. Use a Type to decode the data required by that work, then pass the decoded value to a Task. A pure synchronous conversion that can fail can be an ordinary function returning Result.
Keeping those responsibilities separate prevents an Evolu Type from becoming a hidden application workflow. It also keeps validation deterministic, dependency-free, immediately composable, and straightforward to test.
What if only decoding is needed?
Use fromUnknown for unknown data. For typed application data, call from
at the boundary its input type proves, or use orThrow or orNull for a
flat conversion from Input. The canonical to encoder still keeps the Type
lawful and composable with transformations and structural Types. A genuinely
irreversible operation is a separate function or Task, not a Type
transformation.
Extends
Extended by
TransformTypeObjectTagTypeInstanceOfTypeLiteralTypeUnionTypeBrandTypeTableIdArrayTypeSetTypeTupleTypeRecordTypeDiscriminatedUnionTypeLazyTypeJsonValueTypeJsonObjectType
Properties
[concreteTypeSymbol]
readonly [concreteTypeSymbol]: true;
Defined in: packages/common/src/Type.ts:483
[customFromSymbol]
readonly [customFromSymbol]: CustomFrom;
Defined in: packages/common/src/Type.ts:487
Overrides
[errorsSymbol]
readonly [errorsSymbol]: Errors;
Defined in: packages/common/src/Type.ts:479
Overrides
[identityEncodingSymbol]
readonly [identityEncodingSymbol]: IdentityEncoding;
Defined in: packages/common/src/Type.ts:490
Overrides
TypeNode.[identityEncodingSymbol]
~standard
readonly ~standard: Props<Input, Output>;
Defined in: packages/common/src/Type.ts:430
Standard Schema V1 interoperability.
Validation runs the complete fromUnknown pipeline synchronously and
reports every structured failure as a localized message with a separate
property path.
Overrides
CanonicalInput
CanonicalInput: CanonicalInput;
Defined in: packages/common/src/Type.ts:415
The statically known subtype of Input returned by the complete to
operation.
Compared with Input, CanonicalInput can exclude invalid candidates and alternative representations that encoding cannot emit. It can remain wider than the values actually emitted when a refinement follows an arbitrary transformation. Structural Types derive it from the CanonicalInput of their contained Types.
This is a type-only phantom property. Use it through typeof Type.CanonicalInput; it does not exist at runtime.
Overrides
Error
Error: Error;
Defined in: packages/common/src/Type.ts:409
The error introduced at this Type node.
This is a type-only phantom property. Use it through typeof Type.Error;
it does not exist at runtime.
Overrides
formatError
readonly formatError: TypeErrorFormatter<Errors>;
Defined in: packages/common/src/Type.ts:510
Formats an error returned by fromUnknown or from as one human-readable
message. Built-in Types use English; localizeTypes derives Types
with localized formatters.
Structural errors retain nested errors and their locations in the typed error value. This formatter does not encode paths or enumerate nested errors in its message.
from
readonly from: [CustomFrom] extends [never] ? [Parent] extends [P] ? FromOperation<Output, Error, P> : (value: Output, options?: ValidationOptions) => Result<Output, never> : CustomFrom;
Defined in: packages/common/src/Type.ts:547
Runs the remaining Type pipeline from a typed boundary.
from accepts this Type's Output. Its first .parent accepts the
immediate parent Output, and each additional suffix moves the boundary one
Type toward the root. The deepest suffix accepts the root Output.
Every entry point asserts its selected boundary before running the
remaining pipeline. Assertion failures throw because they indicate a
developer error. The Error message identifies the expected boundary Type,
and its cause preserves the structured validation error. Only failures
introduced after that boundary are returned through Result.
fromUnknown
readonly fromUnknown: (value: unknown, options?: ValidationOptions) => Result<Output, Errors>;
Defined in: packages/common/src/Type.ts:496
Decodes an unknown value through the complete Type pipeline.
Overrides
Input
Input: Input;
Defined in: packages/common/src/Type.ts:407
The complete typed decoding boundary accepted by orThrow, orNull, and
the deepest available from operation.
Input includes candidates that validation can reject and noncanonical representations that decoding can normalize.
This is a type-only phantom property. Use it through typeof Type.Input;
it does not exist at runtime.
Overrides
is
readonly is: (value: unknown) => value is Output;
Defined in: packages/common/src/Type.ts:532
Checks whether an unknown value is a valid semantic Output.
This is an exact Output-membership check, not a test of whether an encoded Input could be decoded. It can be used directly as a TypeScript type guard, including as an Array filter predicate.
Example
import { Int64FromInt64String, type Int64 } from "@evolu/common";
const values: ReadonlyArray<unknown> = [42n, "42", null];
const integers = values.filter(Int64FromInt64String.is);
expectTypeOf(integers).toEqualTypeOf<globalThis.Array<Int64>>();
expect(Int64FromInt64String.is(42n)).toBe(true);
expect(Int64FromInt64String.is("42")).toBe(false);
Overrides
name
readonly name: Name;
Defined in: packages/common/src/Type.ts:421
The name identifying this Type node.
Overrides
orNull
readonly orNull: (value: Input) => Output | null;
Defined in: packages/common/src/Type.ts:625
Shorthand for calling getOrNull with the result of the deepest
from operation, which accepts this Type's Input.
The typed Input boundary is asserted before the remaining pipeline runs.
A boundary violation throws a developer error directly; getOrNull maps
only a validation error returned after that boundary to null.
Type.orNull.parent(value) does not exist. To return null after starting
from a typed boundary, call getOrNull with the corresponding from
operation.
Use orNull when absence is the complete meaning of failure and the error
is intentionally irrelevant. Use fromUnknown or a typed from operation
when the caller needs to inspect, format, or otherwise handle the error.
Example
import { getOrNull, minLength, String } from "@evolu/common";
const NonEmptyString = minLength(1)(String);
const value = NonEmptyString.orNull("Evolu");
// Equivalent because `from.parent` is this Type's deepest `from` operation:
const sameValue = getOrNull(NonEmptyString.from.parent("Evolu"));
orThrow
readonly orThrow: (value: Input, options?: ValidationOptions) => Output;
Defined in: packages/common/src/Type.ts:594
Shorthand for calling getOrThrow with the result of the deepest
from operation, which accepts this Type's Input.
The typed Input boundary is asserted before the remaining pipeline runs.
A boundary violation throws a developer error directly; getOrThrow maps
only a validation error returned after that boundary.
Type.orThrow.parent(value) does not exist. To throw after starting from a
typed boundary, call getOrThrow with the corresponding from operation.
Use orThrow for startup and configuration, module constants, test
fixtures, and internal invariants where failure must stop the current flow.
Prefer fromUnknown or a typed from operation for ordinary application
input whose validation failure can be reported or recovered from.
Example
import { getOrThrow, minLength, String } from "@evolu/common";
const NonEmptyString = minLength(1)(String);
const value = NonEmptyString.orThrow("Evolu");
// Equivalent because `from.parent` is this Type's deepest `from` operation:
const sameValue = getOrThrow(NonEmptyString.from.parent("Evolu"));
Output
Output: Output;
Defined in: packages/common/src/Type.ts:408
The semantic value produced by decoding and accepted by bare from and
to.
This is a type-only phantom property. Use it through typeof Type.Output;
it does not exist at runtime.
Overrides
parent
readonly parent: Parent;
Defined in: packages/common/src/Type.ts:493
The one preceding Type node, or null for a root Type.
Overrides
to
readonly to: [Parent] extends [P] ? ToOperation<Output, CanonicalInput, P> : (value: Output) => CanonicalInput;
Defined in: packages/common/src/Type.ts:561
Asserts and encodes an Output toward its canonical Input
representation.
to runs the complete encoding pipeline. Its first .parent stops at the
immediate parent Output, and each additional suffix stops one Type closer
to the root. Every entry point accepts this Type's Output.