API reference@evolu/commonType › 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 from and its .parent entry points to connect precise producer and consumer contracts while preserving typed remaining errors; reserve fromUnknown for genuinely unknown values.
  • Lawful codecs – Types partially decode Input to Output and totally encode every legitimate Output to CanonicalInput, the statically known subtype of Input returned 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

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

TypeNode.[customFromSymbol]


[errorsSymbol]

readonly [errorsSymbol]: Errors;

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

Overrides

TypeNode.[errorsSymbol]


[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

TypeNode.~standard


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

TypeNode.CanonicalInput


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

TypeNode.Error


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

TypeNode.fromUnknown


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

TypeNode.Input


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

TypeNode.is


name

readonly name: Name;

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

The name identifying this Type node.

Overrides

TypeNode.name


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

TypeNode.Output


parent

readonly parent: Parent;

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

The one preceding Type node, or null for a root Type.

Overrides

TypeNode.parent


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.