Diagnostics

All Diagnostics

Every diagnostic RunTypes can report, searchable by any text.

Every diagnostic RunTypes can report, by name, grouped by feature. Search any text, or filter by level and feature. The levels are explained in Error Levels.

Project configuration

Raised when the project tsconfig the tooling was pointed at cannot be loaded.

config-lib-missing-baseRuntimeError
The project `lib` declares no base ECMAScript library (loaded: {libs}), so core globals like `Array` are missing and reflected types cannot be trusted.

Your tsconfig lib names no base ECMAScript edition, so Array, Object, String and the other core globals are never declared. Then number[] becomes an empty object and its validator would accept any value, so mion stops. Name a base edition in lib (["ES2022"], or ["ES2022", "DOM"] for browser code), or remove lib and let target pick it. A feature entry such as "esnext.disposable" cannot replace a base edition.

{"compilerOptions": {"lib": ["ES2022"]}}
config-output-outside-out-dirError
`mion compile` refused to write {file}: it lands outside outDir ({outDir}) because its source sits outside rootDir; move rootDir up so every file of the program is under it, or reach that module through its package name.

Like tsc, mion compile writes each file under outDir, in the same folders it has under rootDir. A file imported from outside rootDir (a paths entry into a sibling package, or a relative import above the source folder) has no place there, so it is not written and the compile fails. Set rootDir to a folder that holds every file of the program, or import that module by its package name. tsc reports it as TS6059.

config-tsconfig-not-loadedError
Project tsconfig failed to load ({reason}): the build, the linter, and the CLI all read this config, so nothing can run until it loads.

Your tsconfig (the one you named, or the one found next to your project) is missing or cannot be parsed. mion reads your types through it, so it stops rather than guess with defaults. Fix the tsconfig (the message names the first problem), or point to the right file with the tsconfig setting of the build plugin or linter, or the --tsconfig flag.

Markers and call sites

Raised at a marker call, before the build can turn your type into a function.

marker-any-from-unresolved-importRuntimeError
Marker type resolved to `any` because this file has an unresolved import (`{import}`): the generated functions would silently accept anything.

An import in this file did not resolve, so its type became any at this marker. A validator over any accepts everything, a mock returns undefined, and encoders pass values through unchanged. Usual causes: an import without a file extension under moduleResolution: NodeNext, a missing dependency, or a paths alias missing from the tsconfig mion reads. Fix the import, or give mion the same tsconfig as your bundler. For a real any, use an alias like type Loose = any declared in a file whose imports all resolve.

import {User} from './user.runtype.ts';
marker-any-from-unresolved-nameRuntimeError
Marker type resolved to `any` that was never written: `{name}` failed to resolve (or its declaration references a name that does not), so the generated functions would silently accept anything.

A type name at this marker did not resolve, so TypeScript read it as any and the generated functions would accept every value. A real any, written as any or through an alias like type Loose = any, is always allowed. Usual causes: a typo, a dependency whose types are not installed, or a .d.ts file the tsconfig include or files does not cover. After adding a .d.ts, restart the dev server or your editor's linter.

import {getRunTypeId} from '@mionjs/run-types';
export const id = getRunTypeId<{value: Missing}>();
// src/ambient.d.ts, matched by the tsconfig include set
declare interface Ambient { a: string; b: number }
marker-calls-function-for-typeWarning
`{function}()` is being called at runtime just so the marker can read its return type: side effects, throws, or async work run for nothing.

A marker given a function call, like createValidateFn(getUser()) or getRunTypeId(getUser()), runs that call at runtime only to read its type. Use the type form with ReturnType, or pass a value you already have.

const isUser = createValidateFn<ReturnType<typeof getUser>>();

// or pass an existing value of that type
const userTypeId = getRunTypeId(existingUser);
marker-comptime-arg-forbidden-constructRuntimeError
`CompTimeArgs<T>` literal contains a forbidden construct ({construct}). Only literals and nested literals are allowed.

Every part of a CompTimeArgs<T> literal must be a string, number, bigint, boolean, null, undefined, regex, arrow function, object or array literal, or a const that points to one. Computed property names, function calls, ternaries and template strings with ${} are rejected. A spread works only on a const (or imported) object literal inside an object, or an array literal inside an array. If the value is only known at runtime, use the untracked version of the function: getPureFnByKey, hasPureFnByKey and getCompiledPureFnByKey instead of getPureFn, hasPureFn and getCompiledPureFn.

const base = {strict: true} as const;
const options = {...base, mode: 'unsafe'} as const;
marker-comptime-arg-not-literalRuntimeError
`CompTimeArgs<T>` argument must be a literal at the call site, or a `const` whose initializer is itself entirely literal (a same-module or imported `const` both work).

The build reads a CompTimeArgs<T> argument from your source, so it must be a literal or a const whose value is all literals, declared in the same file or imported. Function calls, property access, ternaries and let or var variables cannot be read, and an object const needs as const (marker-comptime-arg-widened-const). If the value is only known at runtime, use the untracked version of the function: getPureFnByKey, hasPureFnByKey and getCompiledPureFnByKey instead of getPureFn, hasPureFn and getCompiledPureFn.

const isUser = createValidateFn<User>(undefined, {mode: 'unsafe'});

// or a const of literals, here or in another module
const opts = {mode: 'unsafe'} as const;
const isUserToo = createValidateFn<User>(undefined, opts);
marker-comptime-arg-too-deepRuntimeError
`CompTimeArgs<T>` literal nesting exceeds the depth cap (16), refactor to flatten.

A CompTimeArgs<T> literal can nest at most 16 levels deep. Flatten it, or split it into several smaller CompTimeArgs<T> arguments.

marker-comptime-arg-widened-constRuntimeError
`CompTimeArgs<T>` value comes from a `const` with a widened (non-literal) member ({member}); declare the const `as const`.

A const passed as a CompTimeArgs or CompTimeFnArgs argument (a whole options object, or a builder child) must keep literal types. Without as const, {strategy: 'mutate'} widens to {strategy: string}, and TypeScript can pick one function variant while the build injects another. Add as const to the declaration.

const preset = {strategy: 'mutate'} as const;
createJsonEncoderFn(undefined, preset);
marker-duplicate-function-familyInfo
`InjectTypeFnArgs` names the function family `{family}` more than once; remove the duplicate key.

An InjectTypeFnArgs marker lists each function family once. A repeat adds nothing and is usually a copy and paste slip. The build ignores it, so remove it from the list.

import type {InjectTypeFnArgs} from '@mionjs/run-types';
type Handler = (ctx: unknown, ...rest: any[]) => unknown;
function route<H extends Handler>(handler: H, fns?: InjectTypeFnArgs<Parameters<H>, 'validationErrors', 'jsonDecoder', 'validationErrors'>) {
  return {handler, fns};
}
export const lenRoute = route((ctx: unknown, name: string) => name.length);
function route<H extends Handler>(
  handler: H,
  fns?: InjectTypeFnArgs<Parameters<H>, 'validationErrors', 'jsonDecoder', 'jsonEncoder'>,
) {
  return {handler, fns};
}
marker-generic-missing-type-argumentError
Generic type `{type}` is used without its required type argument(s): parameter `{typeParam}` has no default, so the type cannot resolve to an id. See Related for where `{typeParam}` is declared.

TypeScript rejects this too (TS2314), but dev server builds skip the type check, and the type would become any with a validator that accepts everything. Pass the missing type argument, or give the parameter a default, used wherever the type is written without arguments.

const isA = createValidateFn<A<string>>();

// or give the parameter a default
interface A<S extends string = string> { a: S }
marker-in-generic-functionError
Marker call is inside a generic function: the type argument is unresolved, so no id can be computed at build time.

Inside a generic function the marker sees T, not a real type, and T changes with each caller, so no single id fits. Call the marker where the type is known, like createValidateFn<User>(), or let the caller pass the id in with InjectRunTypeId<T>.

function makeChecker<T>(id: InjectRunTypeId<T>) {
  return createValidateFn<T>(id);
}
const isUser = makeChecker<User>(getRunTypeId<User>());
marker-self-instantiating-genericError
Type `{type}` re-instantiates itself with fresh type arguments at every level (a self-instantiating generic), so its structural id never resolves. Reflect a monomorphic shape instead.

A generic method that returns its own type with new type arguments, like map<U>(fn: (x: T) => U): Iter<U>, makes the type grow forever, and renaming the arguments does not help. Write a fully resolved recursive type instead, or reflect only the data, since validators skip methods. Normal generics like Map<string, User>, and generic methods that do not return their own type, work fine.

interface NumberIter { map(fn: (x: string) => number): NumberIter }
marker-temporal-lib-missingRuntimeError
Temporal type `{type}` resolved to `any`: the Temporal lib isn't in your tsconfig `lib`, so the generated validator would accept any value.

Temporal.* types only work when the Temporal lib is loaded. Without it the type becomes any and the validator accepts everything. Add "ESNext.Temporal" to lib in your tsconfig.

{"compilerOptions": {"lib": ["ES2023", "ESNext.Temporal"]}}
marker-type-id-collisionError
Two different types get the same id `{id}`: `{firstType}` from {firstSite}, and `{secondType}` here. Raise the `hashLength` option to {hashLength} so every type keeps its own id.

Two types got the same short id, so the build cannot tell them apart and stops. Raise hashLength (the exact id length) by one in your tsconfig plugin entry or on the build plugin; each extra character gives 62 times more ids. The error names both types, the length to use, and the call that took the id first.

{"compilerOptions": {"plugins": [{"name": "mion", "hashLength": 8}]}}
marker-type-too-deepError
This type is too deeply nested to reflect: computing its structural id hit the recursion depth cap, so the build stops here instead of crashing.

The type is nested hundreds of levels deep, written out or generated, with no named type that repeats, and mion stops at a depth far past any real shape. Use a smaller part of the type (such as the item or data you actually send), or recurse through a named type that refers to itself, like a plain recursive interface.

marker-unknown-function-familyError
`InjectTypeFnArgs` names `{family}`, which is not a function family{hint}

Each name in an InjectTypeFnArgs marker must be a function family, like validationErrors. An unknown name compiles nothing, so at runtime the call falls back to its no-plugin behaviour. The old short tags (val, verr, pjs) are not accepted; use the full name. The message suggests the closest name when there is one.

import type {InjectTypeFnArgs} from '@mionjs/run-types';
type Handler = (ctx: unknown, ...rest: any[]) => unknown;
function route<H extends Handler>(handler: H, fns?: InjectTypeFnArgs<Parameters<H>, 'verr'>) {
  return {handler, fns};
}
export const lenRoute = route((ctx: unknown, name: string) => name.length);
function route<H extends Handler>(
  handler: H,
  fns?: InjectTypeFnArgs<Parameters<H>, 'validationErrors'>,
) {
  return {handler, fns};
}
marker-unresolved-type-parameterError
Type argument contains the unresolved type parameter `{typeParam}`: a generic must be fully resolved at the marker call, so no id can be computed. See Related for where `{typeParam}` is declared.

A type parameter of the surrounding generic changes with each call, so no single id fits, and a default on the parameter does not help inside the generic's body. Resolve the generic first, like type BoxString = Box<string>, or let the caller pass the id in (see marker-in-generic-function). Generic methods on a concrete type, like find<T>(query: string): T[], are fine.

interface Box<T> { value: T }
type BoxString = Box<string>;
const isBoxString = createValidateFn<BoxString>();
marker-untrusted-packageRuntimeError
`{name}` here was declared by `{package}`, which this project does not trust as a marker package, so the type argument was dropped and this call reflects `unknown`.

A marker type counts only when a trusted package declares it, so a same-named type of your own is ignored. This one comes from an untrusted package, so the call builds for unknown and its validator accepts everything. Add the package to markers.packages in your tsconfig plugin entry, markers on the build plugin, or --marker-packages on the command line (@mionjs/run-types stays trusted). A package that re-exports the markers from @mionjs/run-types needs no setting, so check whether it meant to.

{"compilerOptions": {"plugins": [{"name": "mion", "markers": {"packages": ["@my-org/runtypes-markers"]}}]}}
marker-untyped-private-memberRuntimeError
Private member `{member}` of class `{class}` has no type in its declaration file, so the generated functions would accept any value for it. Build the class's package with `mion compile`, which keeps private member types, or read it from its TypeScript sources.

A class read from a .d.ts file has a private member with no type. Plain tsc writes every private field and method this way, so RunTypes cannot check it and the build fails. Build the package with mion compile, which keeps the type by writing the member as protected (outside code still cannot read it), or point its types at its TypeScript sources.

import {getRunTypeId} from '@mionjs/run-types';
declare class Account { id: string; private balance; }
export const id = getRunTypeId<Account>();
// package.json of the class's package
{"scripts": {"build": "mion compile --tsconfig tsconfig.build.json"}}

Comments

Raised when a @mion-expect-error or @mion-downgrade-error comment is itself wrong.

comment-downgrade-error-already-warningInfo
`@mion-downgrade-error {name}` does nothing: that code is already a warning or info, so it was never halting your build.

A @mion-downgrade-error comment named a warning or info code, which never stops the build. Delete it, or use @mion-expect-error to hide the code completely.

// @mion-expect-error validate-non-data-property-dropped
comment-downgrade-error-not-allowedWarning
`@mion-downgrade-error {name}` cannot lower that code: the build produces no code for it, so carrying on would ship missing output.

A @mion-downgrade-error comment named an Error code, which always stops the build. The build produced no code for that call, so going on would ship a call that throws. Only a RuntimeError can be downgraded, by comment or by the downgradeErrors setting. Fix the reported call instead.

comment-downgrade-error-unknown-nameWarning
`@mion-downgrade-error {name}` names a diagnostic that does not exist; check the spelling against the name in the message you are lowering.

A @mion-downgrade-error comment names a code mion does not have, usually a typo, so it lowers nothing. Copy the code from the message you are lowering, for example validate-symbol-root in error validate-symbol-root: Type ... can never be validated.

// @mion-downgrade-error validate-symbol-root
comment-downgrade-error-unusedWarning
Unused `@mion-downgrade-error {name}`: nothing was reported on the line below it, so the comment is stale and can be deleted.

A @mion-downgrade-error comment lowered nothing: the code it names was not reported on the line below. Like @mion-expect-error, a comment left behind after a fix is reported. Delete the comment, or correct the code it names.

// @mion-downgrade-error validate-symbol-root
export const report = createValidateFn<symbol>();
comment-expect-error-not-allowedWarning
`@mion-expect-error {name}` cannot silence that code: it is reported even when everything else is silenced.

A @mion-expect-error comment named a code that is always reported. An Error code means the build produced no code for that call, so hiding it would ship missing output. The comment-* names check these comments, so they cannot be hidden either. Fix the reported call instead.

comment-expect-error-unknown-nameWarning
`@mion-expect-error {name}` names a diagnostic that does not exist; check the spelling against the name in the message you are silencing.

A @mion-expect-error comment names a code mion does not have, usually a typo, so it hides nothing. Copy the code from the message you are hiding, for example validate-symbol-root in error validate-symbol-root: Type ... can never be validated.

// @mion-expect-error validate-symbol-root
comment-expect-error-unusedWarning
Unused `@mion-expect-error {name}`: nothing was reported on the line below it, so the comment is stale and can be deleted.

A @mion-expect-error comment hid nothing: the code it names was not reported on the line below. Like @ts-expect-error, a comment left behind after a fix is reported. Delete the comment, or correct the code it names.

// @mion-expect-error validate-symbol-root
export const report = createValidateFn<symbol>();

Validation

From createValidateFn and createGetValidationErrorsFn.

validate-any-accepts-allInfo
`validate` on `any` / `unknown` always returns true: the validator accepts every value.

any and unknown have no shape to check, so the validator accepts every value, including ones you meant to reject. Narrow the type to the shape you expect.

import {createValidateFn} from '@mionjs/run-types';
export const isAnything = createValidateFn<unknown>();
const isUser = createValidateFn<User>(); // instead of <unknown>
validate-function-property-droppedInfo
Property `{property}` is a function: `validate` does not handle function values, so this property is silently not validated.

A function holds no data, so validation skips the property and still checks the others. You see this for an optional function property or one in a union member. A required one reports validate-method-dropped. Remove the property, or replace it with the data it would produce.

import {createValidateFn} from '@mionjs/run-types';
interface Button { label: string; onClick?: () => void }
export const isButton = createValidateFn<Button>();
validate-function-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Your type is a function, a method, or an interface with a call signature. A function is code, not data. Validate what it takes or returns instead.

import {createValidateFn} from '@mionjs/run-types';
export const isData = createValidateFn<() => void>();
const isArgs = createValidateFn<Parameters<typeof handler>>();
const isResult = createValidateFn<Awaited<ReturnType<typeof handler>>>();
validate-method-droppedInfo
Method `{method}` is silently not validated by `validate`: methods aren't data.

A method like greet(): string, or a function property like onClick: () => void, is not data. Validation skips it and still checks the rest of the type. To check its result, store that result in a data property.

import {createValidateFn} from '@mionjs/run-types';
interface User { name: string; greet(): string; }
export const isUser = createValidateFn<User>();
validate-non-data-property-droppedInfo
Property `{property}` has a non-serialisable value type (symbol, Promise, or a non-serialisable built-in): `validate` drops it, so this property is silently not validated.

A property holding a symbol, a Promise or a built-in class with no data form (a typed array, ArrayBuffer, URLSearchParams and similar) is skipped, so { id: symbol } validates as {}. The other properties are still checked. A value that holds one inside, like symbol[] or Map<string, symbol>, cannot be dropped, so the whole type fails with the matching error, such as validate-symbol-root.

import {createValidateFn} from '@mionjs/run-types';
interface Box { id: symbol; name: string; }
export const isBox = createValidateFn<Box>();
validate-non-data-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Your type is a built-in class with no JSON form, such as URLSearchParams, Intl.DateTimeFormat, WeakMap, Promise, RegExp, Buffer or a typed array like Uint8Array. Only Date, Map, Set, URL and the Temporal types are supported. Validate plain data instead, or convert the value first (params.toString(), Array.from(bytes)).

import {createValidateFn} from '@mionjs/run-types';
export const isData = createValidateFn<Uint8Array>();
const bytes = Array.from(myUint8Array);
const isData = createValidateFn<number[]>();
validate-static-droppedInfo
Static member `{member}` is silently not validated by `validate`: statics aren't part of instance data.

Static members belong to the class, not its instances, and validation checks instance data only. The rest of the class is still checked.

import {createValidateFn} from '@mionjs/run-types';
class Config { static version = 1; name = ''; }
export const isConfig = createValidateFn<Config>();
validate-symbol-key-droppedInfo
Symbol-keyed property `{property}` is silently not validated by `validate`: symbol keys aren't JSON-representable.

JSON has string keys only, so validation skips a symbol-keyed property. Use a string key if the property holds real data.

import {createValidateFn} from '@mionjs/run-types';
const tag = Symbol('tag');
interface Item { id: string; [tag]: string }
export const isItem = createValidateFn<Item>();
interface Item {
  id: string; // instead of [Symbol.for('id')]: string
}
validate-symbol-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Your type is a symbol, or one named symbol such as typeof mySymbol. A symbol's identity is lost when it crosses a network, a worker or a process. Use a string union instead.

import {createValidateFn} from '@mionjs/run-types';
export const isData = createValidateFn<symbol>();
type Status = 'pending' | 'active' | 'done';
validate-union-member-droppedInfo
Union member(s) of type `{type}` can't be represented as data: `validate` drops them, so the union is validated as its remaining members.

Validation drops union members with no data form (a symbol, a function, a Promise, or a built-in class that is not data), so Date | symbol validates as Date. If no member has a data form, the whole type fails with validate-non-data-root, validate-symbol-root or validate-function-root.

import {createValidateFn} from '@mionjs/run-types';
export const isData = createValidateFn<Date | symbol>();
validation-errors-any-accepts-allInfo
`validationErrors` on `any` / `unknown` always returns an empty error array: nothing is checked.

Same as validate-any-accepts-all, for createGetValidationErrorsFn. any and unknown have nothing to check, so the error list is always empty. Narrow the type to the shape you expect.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
export const errorsOf = createGetValidationErrorsFn<unknown>();
const errorsOf = createGetValidationErrorsFn<User>(); // instead of <unknown>
validation-errors-function-property-droppedInfo
Property `{property}` is a function: `validationErrors` does not handle function values, so this property is silently not checked.

Same as validate-function-property-dropped, for createGetValidationErrorsFn. A function holds no data, so the property is left out of the error report and the others are still checked. You see this for an optional function property or one in a union member. A required one reports validation-errors-method-dropped. Remove the property, or replace it with the data it would produce.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
interface Button { label: string; onClick?: () => void }
export const getButtonErrors = createGetValidationErrorsFn<Button>();
validation-errors-function-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Same as validate-function-root, for createGetValidationErrorsFn. Your type is a function, a method, or an interface with a call signature, which is code, not data. Check what it takes or returns instead.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
export const errorsOf = createGetValidationErrorsFn<() => void>();
const argsErrors = createGetValidationErrorsFn<Parameters<typeof handler>>();
const resultErrors = createGetValidationErrorsFn<Awaited<ReturnType<typeof handler>>>();
validation-errors-method-droppedInfo
Method `{method}` is silently not checked by `validationErrors`: methods aren't data.

Same as validate-method-dropped, for createGetValidationErrorsFn. A method like greet(): string, or a function property like onClick: () => void, is not data, so it is left out of the error report. To check its result, store that result in a data property.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
interface User { name: string; greet(): string; }
export const errorsOf = createGetValidationErrorsFn<User>();
validation-errors-non-data-property-droppedInfo
Property `{property}` has a non-serialisable value type (symbol, Promise, or a non-serialisable built-in): `validationErrors` drops it, so this property is silently not checked.

Same as validate-non-data-property-dropped, for createGetValidationErrorsFn. A property holding a symbol, a Promise or a built-in class with no data form (a typed array, ArrayBuffer, URLSearchParams and similar) is left out of the error report, and the other properties are still checked. A value that holds one inside, like symbol[] or Map<string, symbol>, cannot be dropped, so the whole type fails with the matching error, such as validation-errors-symbol-root.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
interface Box { id: symbol; name: string; }
export const errorsOf = createGetValidationErrorsFn<Box>();
validation-errors-non-data-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Same as validate-non-data-root, for createGetValidationErrorsFn. Your type is a built-in class with no JSON form, such as URLSearchParams, Intl.DateTimeFormat, WeakMap, Promise, RegExp, Buffer or a typed array like Uint8Array. Only Date, Map, Set, URL and the Temporal types are supported. Check plain data instead, or convert the value first (params.toString(), Array.from(bytes)).

import {createGetValidationErrorsFn} from '@mionjs/run-types';
export const errorsOf = createGetValidationErrorsFn<Uint8Array>();
const bytes = Array.from(myUint8Array);
const errorsOf = createGetValidationErrorsFn<number[]>();
validation-errors-static-droppedInfo
Static member `{member}` is silently not checked by `validationErrors`: statics aren't part of instance data.

Same as validate-static-dropped, for createGetValidationErrorsFn. Static members belong to the class, not its instances, so they are left out of the error report.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
class Config { static version = 1; name = ''; }
export const errorsOf = createGetValidationErrorsFn<Config>();
validation-errors-symbol-key-droppedInfo
Symbol-keyed property `{property}` is silently not checked by `validationErrors`: symbol keys aren't JSON-representable.

Same as validate-symbol-key-dropped, for createGetValidationErrorsFn. JSON has string keys only, so a symbol-keyed property is left out of the error report. Use a string key if the property holds real data.

interface Item {
  id: string; // instead of [Symbol.for('id')]: string
}
validation-errors-symbol-rootRuntimeError
Type `{type}` can never be validated: the generated function will always fail.

Same as validate-symbol-root, for createGetValidationErrorsFn. Your type is a symbol, or one named symbol such as typeof mySymbol, and each symbol's identity is lost when it crosses a network, a worker or a process. Use a string union instead.

import {createGetValidationErrorsFn} from '@mionjs/run-types';
export const errorsOf = createGetValidationErrorsFn<symbol>();
type Status = 'pending' | 'active' | 'done';

Serialization

From the JSON families, plus members that can never be data.

data-non-enumerable-requiredWarning
Property `{property}` is tagged @nonEnumerable but is required: the guard only applies to optional properties, so the tag has no effect. Make it optional (`{property}?`) or remove the tag.

The @nonEnumerable tag skips a property that is not an enumerable own property of the value, but only when the property is optional. On a required property the tag is ignored and the property is always sent. Add ? to the property, or remove the tag.

interface Session {
  /** @nonEnumerable */ token?: string;
}
data-proto-property-droppedWarning
Property `{property}` can never be data and is dropped: the rest of the type still works.

A property named __proto__ is left out of every generated function, anywhere in the type (nested objects, array items and Map values too). Setting that key changes the object's prototype instead of storing a value, so it never exists at runtime even though TypeScript accepts it. prototype and constructor are normal names and are kept. Rename the property to keep the data.

import {createValidateFn} from '@mionjs/run-types';
interface Settings { ok: number; __proto__: string }
export const isSettings = createValidateFn<Settings>();
interface Settings { ok: number; parent: string }
json-prepare-clone-function-property-droppedInfo
Property `{property}` is a function: the JSON encoder does not handle function values, so this property is silently not encoded.

A function has no JSON form, so the encoder leaves this property out and encodes the rest as usual. This is expected: only data is encoded.

json-prepare-clone-function-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

A function has no JSON form, so there is nothing to encode. Use the data the function returns. A method or function property inside an object is dropped instead (json-prepare-clone-method-dropped, or json-prepare-clone-function-property-dropped when optional or in a union member).

interface User {
  name: string; // not getName: () => string
}
json-prepare-clone-method-droppedInfo
Method `{method}` is silently not encoded: methods aren't data.

Methods are not data, so the encoder leaves them out and still encodes the rest of the type. To encode a method's result, store it in a data property.

json-prepare-clone-never-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

Your type resolves to never, so no value can ever match it and there is nothing to encode. Use a concrete type that matches your data, or unknown if you accept any value and check it before use. A never property inside an object is dropped instead (json-prepare-clone-non-data-property-dropped).

type Tag = 'pending' | 'active' | 'done';
// or, to accept any value:
type AnyTag = unknown; // check it before use
json-prepare-clone-non-data-property-droppedInfo
Property `{property}` has a non-serialisable value type (symbol, Promise, or a non-serialisable built-in): the JSON encoder drops it, so this property is silently not encoded.

A property holding a symbol, never, a Promise, a RegExp or a standard library class such as a typed array, ArrayBuffer, URLSearchParams or Intl.DateTimeFormat has no JSON form, so the encoder drops it and leaves the rest of the object alone: {a: symbol} encodes as {}. A property holding such a value inside an array or a Map, like symbol[] or Map<string, symbol>, cannot be dropped: you get an error instead (such as json-prepare-clone-symbol-root), and the encoder always fails.

json-prepare-clone-non-data-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

Standard library classes have no JSON form, except Date, Map, Set, URL and the Temporal types. When your type is URLSearchParams, Intl.DateTimeFormat, WeakMap, Promise, RegExp, a typed array or Buffer (or an array of them), there is nothing to encode. Convert the value to plain data, like a string for a URLSearchParams or a number array for a typed array. Inside an object, such a property is dropped instead (json-prepare-clone-non-data-property-dropped).

const query: string = yourParams.toString(); // not a URLSearchParams
const bytes: number[] = Array.from(yourBuffer); // not a typed array
json-prepare-clone-static-droppedInfo
Static member `{member}` is silently not encoded: statics aren't part of instance data.

Static members belong to the class, not its instances, so the encoder leaves them out.

json-prepare-clone-symbol-key-droppedInfo
Symbol-keyed property `{property}` is silently not encoded: symbol keys aren't JSON-representable.

JSON only has string keys, so the encoder leaves out properties keyed by a symbol. Use a string key if you need the value encoded.

interface Item {
  id: string; // not [Symbol.for('id')]: string
}
json-prepare-clone-symbol-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

A symbol's identity is lost in JSON, so it cannot be encoded. Use a string instead, often a union of string literals. A symbol property inside an object is dropped instead (json-prepare-clone-non-data-property-dropped).

type Status = 'pending' | 'active' | 'done'; // not symbol
json-prepare-clone-union-member-droppedInfo
Union member(s) of type `{type}` can't be represented as data: the JSON encoder drops them, so the union is encoded as its remaining members.

Symbols, functions, never, Promise, RegExp and standard library classes such as URLSearchParams or typed arrays have no JSON form, so the encoder drops them from the union: Date | symbol encodes as Date. If every member is dropped, you get the error for the whole type instead (such as json-prepare-clone-symbol-root), and the encoder always fails.

json-prepare-function-property-droppedInfo
Property `{property}` is a function: the JSON encoder does not handle function values, so this property is silently not encoded.

A function has no JSON form, so the encoder leaves this property out and encodes the rest as usual. This is expected: only data is encoded.

json-prepare-function-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

A function has no JSON form, so there is nothing to encode. Use the data the function returns. A method or function property inside an object is dropped instead (json-prepare-method-dropped, or json-prepare-function-property-dropped when optional or in a union member).

interface User {
  name: string; // not getName: () => string
}
json-prepare-method-droppedInfo
Method `{method}` is silently not encoded: methods aren't data.

Methods are not data, so the encoder leaves them out and still encodes the rest of the type. To encode a method's result, store it in a data property.

json-prepare-never-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

Your type resolves to never, so no value can ever match it and there is nothing to encode. Use a concrete type that matches your data, or unknown if you accept any value and check it before use. A never property inside an object is dropped instead (json-prepare-non-data-property-dropped).

type Tag = 'pending' | 'active' | 'done';
// or, to accept any value:
type AnyTag = unknown; // check it before use
json-prepare-non-data-property-droppedInfo
Property `{property}` has a non-serialisable value type (symbol, Promise, or a non-serialisable built-in): the JSON encoder drops it, so this property is silently not encoded.

A property holding a symbol, never, a Promise, a RegExp or a standard library class such as a typed array, ArrayBuffer, URLSearchParams or Intl.DateTimeFormat has no JSON form, so the encoder drops it and leaves the rest of the object alone: {a: symbol} encodes as {}. A property holding such a value inside an array or a Map, like symbol[] or Map<string, symbol>, cannot be dropped: you get an error instead (such as json-prepare-symbol-root), and the encoder always fails.

json-prepare-non-data-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

Standard library classes have no JSON form, except Date, Map, Set, URL and the Temporal types. When your type is URLSearchParams, Intl.DateTimeFormat, WeakMap, Promise, RegExp, a typed array or Buffer (or an array of them), there is nothing to encode. Convert the value to plain data, like a string for a URLSearchParams or a number array for a typed array. Inside an object, such a property is dropped instead (json-prepare-non-data-property-dropped).

const query: string = yourParams.toString(); // not a URLSearchParams
const bytes: number[] = Array.from(yourBuffer); // not a typed array
json-prepare-static-droppedInfo
Static member `{member}` is silently not encoded: statics aren't part of instance data.

Static members belong to the class, not its instances, so the encoder leaves them out.

json-prepare-symbol-key-droppedInfo
Symbol-keyed property `{property}` is silently not encoded: symbol keys aren't JSON-representable.

JSON only has string keys, so the encoder leaves out properties keyed by a symbol. Use a string key if you need the value encoded.

interface Item {
  id: string; // not [Symbol.for('id')]: string
}
json-prepare-symbol-rootRuntimeError
Type `{type}` can never be encoded to JSON: the generated function will always fail.

A symbol's identity is lost in JSON, so it cannot be encoded. Use a string instead, often a union of string literals. A symbol property inside an object is dropped instead (json-prepare-non-data-property-dropped).

type Status = 'pending' | 'active' | 'done'; // not symbol
json-prepare-union-member-droppedInfo
Union member(s) of type `{type}` can't be represented as data: the JSON encoder drops them, so the union is encoded as its remaining members.

Symbols, functions, never, Promise, RegExp and standard library classes such as URLSearchParams or typed arrays have no JSON form, so the encoder drops them from the union: Date | symbol encodes as Date. If every member is dropped, you get the error for the whole type instead (such as json-prepare-symbol-root), and the encoder always fails.

json-restore-function-property-droppedInfo
Property `{property}` is a function: the JSON decoder does not handle function values, so this property is silently not decoded.

A function has no JSON form, so the decoder leaves this property out and decodes the rest as usual. This is expected: only data is decoded.

json-restore-function-rootRuntimeError
Type `{type}` can never be decoded from JSON: the generated function will always fail.

A function has no JSON form, so there is nothing to decode. Use the data the function returns. A method or function property inside an object is dropped instead (json-restore-method-dropped, or json-restore-function-property-dropped when optional or in a union member).

interface User {
  name: string; // not getName: () => string
}
json-restore-method-droppedInfo
Method `{method}` is silently not decoded: methods aren't data.

Methods are not data, so the decoder leaves them out and still decodes the rest of the type. To decode a method's result, store it in a data property.

json-restore-never-rootRuntimeError
Type `{type}` can never be decoded from JSON: the generated function will always fail.

Your type resolves to never, so no value can ever match it and there is nothing to decode. Use a concrete type that matches your data, or unknown if you accept any value and check it before use. A never property inside an object is dropped instead (json-restore-non-data-property-dropped).

type Tag = 'pending' | 'active' | 'done';
// or, to accept any value:
type AnyTag = unknown; // check it before use
json-restore-non-data-property-droppedInfo
Property `{property}` has a non-serialisable value type (symbol, Promise, or a non-serialisable built-in): the JSON decoder drops it, so this property is silently not decoded.

A property holding a symbol, never, a Promise, a RegExp or a standard library class such as a typed array, ArrayBuffer, URLSearchParams or Intl.DateTimeFormat has no JSON form, so the decoder drops it and leaves the rest of the object alone: {a: symbol} decodes as {}. A property holding such a value inside an array or a Map, like symbol[] or Map<string, symbol>, cannot be dropped: you get an error instead (such as json-restore-symbol-root), and the decoder always fails.

json-restore-non-data-rootRuntimeError
Type `{type}` can never be decoded from JSON: the generated function will always fail.

Standard library classes have no JSON form, except Date, Map, Set, URL and the Temporal types. When your type is URLSearchParams, Intl.DateTimeFormat, WeakMap, Promise, RegExp, a typed array or Buffer (or an array of them), there is nothing to decode. Convert the value to plain data, like a string for a URLSearchParams or a number array for a typed array. Inside an object, such a property is dropped instead (json-restore-non-data-property-dropped).

const query: string = yourParams.toString(); // not a URLSearchParams
const bytes: number[] = Array.from(yourBuffer); // not a typed array
json-restore-static-droppedInfo
Static member `{member}` is silently not decoded: statics aren't part of instance data.

Static members belong to the class, not its instances, so the decoder leaves them out.

json-restore-symbol-key-droppedInfo
Symbol-keyed property `{property}` is silently not decoded: symbol keys aren't JSON-representable.

JSON only has string keys, so the decoder leaves out properties keyed by a symbol. Use a string key if you need the value decoded.

interface Item {
  id: string; // not [Symbol.for('id')]: string
}
json-restore-symbol-rootRuntimeError
Type `{type}` can never be decoded from JSON: the generated function will always fail.

A symbol's identity is lost in JSON, so it cannot be decoded. Use a string instead, often a union of string literals. A symbol property inside an object is dropped instead (json-restore-non-data-property-dropped).

type Status = 'pending' | 'active' | 'done'; // not symbol
json-restore-union-member-droppedInfo
Union member(s) of type `{type}` can't be represented as data: the JSON decoder drops them, so the union is decoded as its remaining members.

Symbols, functions, never, Promise, RegExp and standard library classes such as URLSearchParams or typed arrays have no JSON form, so the decoder drops them from the union: Date | symbol decodes as Date. If every member is dropped, you get the error for the whole type instead (such as json-restore-symbol-root), and the decoder always fails.

Unknown keys

From removeUnknownKeys.

unknown-keys-function-sharedWarning
The function in {target} cannot be copied: the copy shares it with the input.

removeUnknownKeys never removes a declared key. A function cannot be copied, so the copy points to the same function as the input. Class methods stay on the prototype instead (unknown-keys-method-not-copied). A function type itself is shared the same way. Pass sharedValues: 'share' to say this is fine, or 'refuse' to make it an error.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
interface Button { label: string; onClick: () => void }
export const removeButtonKeys = createRemoveUnknownKeysFn<Button>();
unknown-keys-method-not-copiedInfo
Method or accessor `{member}` is not copied: the copy keeps the input's prototype, so it still works.

A class copy keeps the input's prototype, so methods and get / set accessors still work without being copied. A function stored in a field (onChange = () => ...) is its own value and is shared instead (unknown-keys-function-shared). The constructor does not run on the copy.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
export class User { name = ''; greet(): string { return this.name; } }
export const removeUserKeys = createRemoveUnknownKeysFn<User>();
unknown-keys-non-data-sharedWarning
The value in {target} cannot be copied (a Promise, a RegExp or a built-in that is not data): the copy shares it with the input.

removeUnknownKeys never removes a declared property. A value it cannot copy, like a Promise, a RegExp or a built-in like URLSearchParams, is shared with the input, so a change through it shows on both. Pass sharedValues: 'share' to say this is fine, 'refuse' to make it an error, or register overrideRemoveUnknownKeys<T>() to copy it yourself.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
interface Link { title: string; pattern: RegExp }
export const removeLinkKeys = createRemoveUnknownKeysFn<Link>();
unknown-keys-object-unionRuntimeError
`removeUnknownKeys` does not support unions with object members: the emitter cannot know which declared shape to rebuild at runtime.

removeUnknownKeys rebuilds a value from its declared shape, and for a union of objects it cannot tell which member the value matches, so the function throws rather than keep unknown keys. Narrow the value first and use one createRemoveUnknownKeysFn<Member>() per member, or turn the union into one object with optional properties.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
type Pet = {kind: 'cat'; meows: boolean} | {kind: 'dog'; barks: boolean};
export const removePetKeys = createRemoveUnknownKeysFn<Pet>();
const removeCatKeys = createRemoveUnknownKeysFn<Cat>();
const removeDogKeys = createRemoveUnknownKeysFn<Dog>();
unknown-keys-private-fieldsRuntimeError
The {target} has `#private` fields, which only its constructor can create: a copy would break its methods, so the function always throws.

A class copy keeps the input's prototype but never runs the constructor, and only the constructor can create #private fields. A method reading one would throw on the copy, so the function always throws instead. Use TypeScript private instead of #, or register overrideRemoveUnknownKeys<T>() to build the copy yourself.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
export class Counter { #count = 0; label = ''; }
export const removeCounterKeys = createRemoveUnknownKeysFn<Counter>();
class Counter { private count = 0; }
unknown-keys-shared-value-refusedRuntimeError
The value in {target} can only be shared with the input and `sharedValues: 'refuse'` is set, so the function always throws.

With sharedValues: 'refuse', a value the copy cannot rebuild (a function, a Promise, a RegExp or a built-in like URLSearchParams) makes the function always throw instead of sharing it with the input. Remove the option to share it with a warning, or change the type.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
interface Button { label: string; onClick: () => void }
export const removeButtonKeys = createRemoveUnknownKeysFn<Button>(undefined, {sharedValues: 'refuse'});
unknown-keys-static-droppedInfo
Static member `{member}` is not part of instance data: `removeUnknownKeys` skips it.

Static members belong to the class, not its instances, so the copy leaves them out.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
export class Config { static version = 1; name = ''; }
export const removeConfigKeys = createRemoveUnknownKeysFn<Config>();
unknown-keys-symbol-keyRuntimeError
Symbol-keyed {target} cannot be copied: the generated code cannot name your symbol, so the function always throws.

The copy is typed as your type, so it must have the symbol-keyed property too. The generated code cannot name a symbol from your code, and copying every symbol on the input would keep undeclared ones, so the function always throws. Use a string key, or a [key: symbol]: V index signature, whose keys are all copied. Symbol-keyed class methods are fine: they stay on the prototype.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
const tag = Symbol('tag');
interface Item { id: string; [tag]: string }
export const removeItemKeys = createRemoveUnknownKeysFn<Item>();
interface Item { id: string; tag: string }
unknown-keys-value-sharedInfo
The value in {target} is shared with the input, as `sharedValues: 'share'` asks.

You passed sharedValues: 'share', so a function or a value the copy cannot rebuild is shared with the input, as asked. This note is hidden unless you show all levels.

import {createRemoveUnknownKeysFn} from '@mionjs/run-types';
interface Button { label: string; onClick: () => void }
export const removeButtonKeys = createRemoveUnknownKeysFn<Button>(undefined, {sharedValues: 'share'});

Type formats

From the pattern and sample checks on a TypeFormat.

format-invalid-paramsRuntimeError
Invalid type-format params: {reason}

A format's settings break one of its rules, so its validator would be wrong or would throw. The message names the rule, for example length used with minLength or maxLength, maxLength below minLength, more than 100 allowedValues, or a pattern that is not a valid JavaScript regex. A string format takes only one of pattern, allowedChars, disallowedChars, allowedValues or disallowedValues, and disallowedChars or disallowedValues need mockSamples. Change the settings the message names.

format-no-js-runtimeRuntimeError
TypeFormat pattern /{pattern}/ cannot be checked: {reason}; pattern validation requires a JavaScript runtime; install one or pass --js-runtime.

mion checks each pattern on a real JavaScript engine (that it compiles and that its mockSamples match), and no JavaScript runtime could be started. Install node or bun (both found on your PATH), or point --js-runtime or MION_JS_RUNTIME at another runtime such as deno. Projects with no patterns never need this.

format-pattern-timeoutRuntimeError
TypeFormat pattern /{pattern}/ could not be evaluated in time: {reason}; the build was not able to tell whether the pattern is safe.

Each mockSample runs through your pattern with a time limit, and this one ran out of time even on a longer retry. Either the pattern backtracks without end (nested repeats such as (a+)+ on a long input are the usual cause), or the machine was too busy. The next build checks again, so a one-off slowdown clears by itself. If it keeps failing on an idle machine, rewrite the pattern without nested repeats, or shorten the sample it times out on.

format-pattern-unreadableRuntimeError
TypeFormat pattern `{pattern}` is a value the build cannot read, so the validator would not check it.

The build reads a pattern from its type. A RegExp value, or a const typed as plain FormatPattern, keeps no source in its type, and a .d.ts keeps only the type. The validator would then not check the pattern at all. Use a registerFormatPattern const without a type annotation, or write the source as a literal.

import {createValidateFn} from '@mionjs/run-types';
import * as TF from '@mionjs/run-types/formats';
declare const sku: {readonly source: string};
export const isSku = createValidateFn<TF.String<{pattern: typeof sku}>>();
const sku = registerFormatPattern({source: '^[A-Z]{3}-[0-9]{4}$'});
type Sku = String<{pattern: typeof sku}>;
// or
type Sku = String<{pattern: {source: '^[A-Z]{3}-[0-9]{4}$'}}>;
format-pattern-unsafeRuntimeError
TypeFormat pattern /{pattern}/ can be made to backtrack exponentially: {reason} (`{input}`); a crafted input would hang the validator.

The pattern can match the same text in more than one way, and JavaScript tries every way before it gives up, so a few dozen almost-matching characters can hang your validator. Make each repeat match one way only, usually by giving the repeated part a boundary the rest cannot match. If the pattern is safe and the check is wrong, add unsafePattern: true to the pattern.

type Words = String<{pattern: {source: '^\\w+(?:\\s\\w+)*$'}}>;
// only if the check is wrong:
type Safe = String<{pattern: {source: '...'; unsafePattern: true}}>;
format-sample-conflictRuntimeError
Two sites share one cache entry for format `{format}` but declare different mockSamples: `{samples}` here vs `{otherSamples}` at {otherSite}. Make the pools identical, or declare one and leave the other out.

Two formats that differ only in their mockSamples share one validator entry, which keeps only one sample list. Which list wins would depend on the order your code is read, so the build stops. Use the same mockSamples in both, or declare them in one and leave them out of the other. If the two types really differ, give them a different pattern, bound or brand.

type A = String<{maxLength: 5; mockSamples: ['aaa']}>;
type B = String<{maxLength: 5; mockSamples: ['aaa']}>; // same samples
// or leave them out: type B = String<{maxLength: 5}>;
format-sample-generation-failedRuntimeError
Cannot auto-generate mockSamples for pattern /{pattern}/: {reason}; declare mockSamples explicitly.

A pattern with no mockSamples gets samples generated at build time, and none came out. Either generation is off (patternSampleCount is 0), the pattern uses something the generator cannot handle (usually a lookaround), or all patternSampleCount times patternSampleRetries tries failed the pattern or its length limits. Declare mockSamples yourself (they are still checked against the pattern), or set patternSampleCount above 0 and raise patternSampleRetries for a heavily constrained pattern.

type Y = String<{pattern: {source: '(?<=x)y'; mockSamples: ['xy']}}>;
format-sample-mismatchRuntimeError
TypeFormat mockSample "{sample}" does not match its pattern /{pattern}/; fix the sample or the pattern.

A mockSample does not match its pattern (checked with the same regex your validator uses), so mock data built from it would fail validation. Change the sample or the pattern so they agree.

type Slug = String<{pattern: {source: '^[a-z]+$'; mockSamples: ['abc', 'xyz']}}>;
format-sample-out-of-boundsRuntimeError
TypeFormat mockSample violates a sibling constraint: {reason}

A mockSample breaks another setting of the same format: length, minLength, maxLength, allowedChars, disallowedChars or disallowedValues. Mock data would then be invalid, or createMockDataFn would drop every sample and throw. Lengths count UTF-16 code units, the same as the validator's .length check. Change the sample or relax the setting.

type B = String<{minLength: 1; pattern: {source: '^b+$'; mockSamples: ['b', 'bb']}}>;

Pure functions

From PureFunction arguments and the purity rules for registerPureFnFactory.

purefn-artifact-conflictError
Pure fn `{fn}` differs between `{first}` and `{second}`.

Two mion-pure-fns/ directories of the same installed package give this id a different body or name, so one is stale or comes from another build. One id must mean one body under one name, so the build stops rather than pick one. Rebuild the package so every output directory has the same mion-pure-fns/, or delete the stale copy.

purefn-artifact-unreadableWarning
`{file}` could not be read as part of a pure-fn artifact: {reason}.

A mion build writes a package's compiled pure functions to mion-pure-fns/ next to its output: an index.json plus one module per function. This file was skipped: the index is not an index or has a format this compiler does not know, or a module it lists is missing or holds nothing for its id. The package may then seem to ship no pure functions, or miss one. Update mion to the version that wrote the directory, or rebuild the package with your version.

purefn-dependency-cycleError
Pure functions circular dependency: `{fn}` (`{id}`) reaches back into `{target}`.

A pure function's id depends on the ids of the pure functions it calls, so two that call each other can never get an id, and at runtime they would call each other forever. Break the cycle: copy the shared part into both, or move it into a third pure function that calls neither.

purefn-dependency-not-idRuntimeError
`{namespace}.{fn}` dependency argument must be a pure-fn id.

utl.usePureFn and utl.getPureFn need a fixed id, so the build can check that the pure function is registered and write the id into the output. Pass the value a registration returned, imported from a file in this build, or a string literal.

import {slugify} from './slug';
const myFn = registerPureFnFactory((utl) => (input) => utl.usePureFn(slugify)(input));
purefn-destructured-paramError
Pure-fn factory `{factory}` uses destructured parameters; only simple identifier params are supported.

A destructured parameter like ({a, b}) or ([x, y]) has no single name the build can refer to. Take one plain parameter and destructure it inside the body.

const myFn = registerPureFnFactory((utl) => (params) => {
  const {a, b} = params;
  return ...;
});
purefn-forbidden-constructRuntimeError
`{construct}` is not allowed inside a pure-fn factory body.

Globals such as eval, Function, fetch, XMLHttpRequest, require, process, globalThis, window and document are blocked in a pure function. They run arbitrary code or depend on an environment the build cannot reproduce. Remove the reference, or pass the value you need in as a parameter.

purefn-id-mismatchError
Explicit pure-fn id `{id}` does not match this registration's computed id `{computedId}`.

The build adds a pure function's id itself, so your source normally passes none. Editing the body changes the id (renaming or moving the function does not), so a hand-written or outdated id registers the body under one id while every reference uses another. Delete the id argument and let the build add it, or regenerate the file the id is imported from.

purefn-imported-or-exportedRuntimeError
`PureFunction<F>` literal must not be imported or exported: the compiled copy must be the only one that can run.

A PureFunction<F> function cannot be imported from or exported to another module. The build compiles the body, and if the original stayed reachable, a caller could run it and get different behaviour from the compiled copy. Write the function inline at the call (a named function is not allowed either, see purefn-not-inline).

registerValidator((v: unknown) => typeof v === 'string');
purefn-not-inlineRuntimeError
`PureFunction<F>` argument must be an INLINE arrow or function expression.

Write the PureFunction<F> function inline at the call, as an arrow or function expression. A named function, even a private const f = ... or function f() {}, is rejected: the build compiles the body ahead of time, and the compiled copy must be the only one that can run. An imported or exported function is reported as purefn-imported-or-exported.

registerValidator((v: unknown) => typeof v === 'string');
purefn-not-registeredRuntimeError
Pure fn `{fn}` is referenced by a RT function but was never registered.

A generated validator or encoder calls a pure function through utl.usePureFn, but no file in the build registers that id, so the code would fail when it runs. Either the file that registers it is outside the build, or its body changed after the id was copied by hand (the id is the package plus a hash of the body). Import the id from the file that registers it with registerPureFnFactory, and make sure that file is part of the build.

import {registerPureFnFactory} from '@mionjs/run-types/runtime';
export const newRunTypeErr = registerPureFnFactory((utl) => (message) => new Error(message));
purefn-package-not-builtError
Pure fn `{fn}` comes from `{package}`, which ships no compiled pure functions.

A pure function imported from another package is built from that package's compiled pure functions (the mion-pure-fns/ directory its mion build writes next to its output) or from its TypeScript sources. This package ships neither. Build it with mion (a bundler plugin or mion compile) and publish its output directory, or publish its sources.

purefn-reads-outer-variableRuntimeError
`{name}` is captured from outer scope inside a pure-fn factory; pure functions can't reach outside their own body.

The build copies a factory's body without the scope around it, so a variable from outside becomes undefined at runtime. Pass it in as a parameter, write its value inside the body if it is a constant, or call another pure function through utl.getPureFn and its id.

import {slugify} from './slug';
// pass the value in
const withRate = registerPureFnFactory((utl) => (rate, value) => value * rate);
// or reach another pure function by its id
const toSlug = registerPureFnFactory((utl) => (value) => utl.getPureFn(slugify)(value));
purefn-uses-awaitRuntimeError
`async`/`await` is not allowed inside a pure-fn factory body.

A pure function must run synchronously so the build can call it at compile time, and async returns a Promise that only resolves at runtime. Make the factory synchronous and do the async work in the caller.

const myFn = registerPureFnFactory((utl) => {
  return (resolvedValue) => transform(resolvedValue);
});
purefn-uses-dynamic-importRuntimeError
`import()` is not allowed inside a pure-fn factory body.

import() loads a module at runtime, and the build needs every dependency up front. Use a top-level import, or pass the module in as a parameter.

purefn-uses-thisRuntimeError
`this` is not allowed inside a pure-fn factory body; pure functions can't depend on a calling context.

A pure function is built on its own at build time, so there is no this to bind. Pass the value in as a parameter, or move the function out of the class or object method that owns this.

const myFn = registerPureFnFactory((utl) => (self, input) => {
  return self.field + input;
});
purefn-uses-yieldRuntimeError
`yield` / generators are not allowed inside a pure-fn factory body.

A generator keeps state between steps, and the build cannot write that out as standalone code. Return an array or a plain iterable instead.

const myFn = registerPureFnFactory((utl) => (input) => {
  return [...computeAll(input)];
});

Overrides

From custom per-type function overrides.

override-duplicateRuntimeError
Duplicate override for `{key}`: there can be exactly one override per (type, function).

Two overrideX<T>() calls target the same type and the same function, for example two overrideValidate<User>(). Which one wins would depend on the order your code is read, so the second one is rejected whatever its body. The Related line points at the first one. Keep one override and merge the logic into it.

overrideValidate<User>((utl) => (value) => checkA(value) && checkB(value));
override-function-not-builtRuntimeError
Override entry `{entry}` references compiled function `{function}` which did not render: this would throw at runtime, so the build stops.

An override points at a generated function that was never written, so calling the override would throw at runtime. This is a mion bug. Delete the cache (node_modules/.cache/mion) or restart the dev server. If it keeps failing, open an issue with the type and the override that trigger it.

override-validate-affects-jsonInfo
Overriding `validate` for this type also changes how JSON decoders narrow unions containing it.

overrideValidate<T>() also changes JSON decoders of any union that contains T, because they call each member's validator to pick the matching branch. If the override should only affect createValidateFn<T>(), give the union members a discriminant so decoders never fall back to member validators.

type Event = {kind: 'click'; x: number} | {kind: 'key'; code: string};

Drizzle

Slim schema isolation and public database type boundaries.

drizzle-mixed-typesRuntimeError
This file defines a slim Mion schema or model and imports heavy Drizzle types or toDrizzle. A client importing a model from this file also type-checks its toDrizzle and query code in a full check, and bundles drizzle-orm on a value import. Move toDrizzle and heavy Drizzle imports to a separate query or database file.

A file that defines slim schemas or models also imports Drizzle types or the toDrizzle bridge. The editor checks a client lazily, so its cost there does not change. But a full type check (tsc or tsgo, as in CI) of a client importing one model from this file also checks every toDrizzle call, relation and query in it, and loads Drizzle's declarations: measured 5 to 20 times more type work. A value import also bundles drizzle-orm into the client. Keep schemas and models in their own file. Import them into a companion file for toDrizzle, database setup, relations and queries. A query or router file may import both kinds of types when it does not define slim schemas or models.

// schema.ts: define slim tables and models
// db.ts: import the slim tables, then call toDrizzle
drizzle-type-not-allowedWarning
Public handler {position} uses a Drizzle ORM type. Use a slim @mionjs/drizzle-orm model or a plain public type to keep client type checking small.

A written public parameter or return type uses a Drizzle ORM type, including nested models and model projections. Client type checking must expand those database types. Use a slim model from @mionjs/drizzle-orm or a plain public type. Drizzle queries and internal types inside the handler are allowed.

import type {User} from './schema.ts';
const list = mion.query(async (_ctx): Promise<User[]> => db.select().from(usersDb));

mion routes

From the rules over mion route, middleware and headersMiddleware handlers, reported as you write them.

rpc-handler-missing-param-typeRuntimeError
mion `{handler}` handler parameter `{param}` has no type annotation; every parameter after the call context travels on the wire and must declare its type.

Every handler parameter after the call context is part of the route's public input, so mion builds a validator and a decoder from its declared type. The call context (the first parameter, the first two for headersMiddleware) never goes over the network. Add a type to the parameter.

mion.route((ctx, name: string): string => `hello ${name}`);
rpc-handler-missing-return-typeRuntimeError
mion `{handler}` handler has no return type annotation; write the type the handler answers with.

mion builds the route's validation and serialization from the handler's declared types, and the client types its call from the same declaration. An inferred return type gives the build nothing to work from. Write the return type on the handler.

mion.route((ctx, name: string): string => `hello ${name}`);
rpc-handler-non-data-propertyWarning
Property `{property}` can never be data and is dropped from every compiled function; rename it.

Writing __proto__ on a plain object changes its prototype instead of adding a key, so the value never round trips, even though TypeScript accepts the type. It is reported on the declaration in any interface, type literal or class, even before a route uses it. Rename the property to keep the data (prototype and constructor are fine).

export interface Settings { ok: number; __proto__: string }
export const settings: Settings = {ok: 1, __proto__: 'x'};
interface Settings { ok: number; parent: string }
rpc-handler-returns-non-rpc-errorRuntimeError
mion `{handler}` handler declares it can answer with `{errorType}`, which is not an `RpcError`; only an `RpcError` (or a subclass such as `FatalError`) carries the mion brand.

A returned RpcError (or any subclass) goes to its own typed slot, and the client gets it typed. A plain Error, a class extending it, or a bare TypedError fails the request and goes to the undeclared @thrownErrors slot, so your declared return type is no longer true. Answer with an RpcError or a FatalError.

mion.route((ctx, id: string): Pet | RpcError<'pet-not-found'> =>
  new RpcError({statusCode: 404, name: 'pet-not-found', publicMessage: 'no pet'}));
rpc-handler-throwsRuntimeError
mion `{handler}` handlers must return errors, not throw them; return an `RpcError` to let the chain continue, or a `FatalError` to stop the request.

A returned error stays in the handler's signature, so the client gets it typed at the call site. A thrown one goes to the undeclared @thrownErrors slot, and the client sees only its public message. A throw caught by try and catch in the same handler is not reported. If a throw is on purpose, add // @mion-expect-error rpc-handler-throws above it and say why.

return new RpcError({statusCode: 404, name: 'not-found', publicMessage: 'no pet'});

Batches

From a batch() call the build cannot read, or two batches that collide.

rpc-batch-argument-out-of-rangeError
`inputFrom()` sits at argument index {index} of route `{route}`, which declares only {count} parameter(s); move the mapping to an argument the route declares.

The server passes a mapped input at the argument position where you wrote inputFrom(), so that position (counted from zero) must be one of the route handler's parameters. Otherwise the server would reject the request at runtime.

// getById(id) takes 1 argument
routes.orders.getById(inputFrom(user, 'toUserId'));
rpc-batch-duplicate-routeError
Route `{route}` is listed twice in this `batch()`; a batch runs each route once, so drop the duplicate or move it into a second batch.

One batch cannot call the same route twice. Results come back by route, so the second call would replace the first. Keep one call per route, or split the calls into two batches.

batch([routes.users.getById(1)]);
batch([routes.users.getById(2)]);
rpc-batch-element-unreadableError
`batch()` element is not a route call the build can read ({reason}); write `routes.a.b(...)` inline or bind it to a `const`/`let` in this file.

Each item in batch([...]) must be a call on the client routes, like routes.users.getById(1), written inline or saved in a const or let in the same file. The build reads them to know which routes run, in what order. A spread, a function parameter, a call on anything other than the routes, or a variable not set to a route call cannot be read.

const user = routes.users.getById(id);
batch([user, routes.orders.list()]);
rpc-batch-id-collisionRuntimeError
Batch id `{id}` is shared by two different batches; reorder the routes of one of them so the ids no longer collide.

Two different batches got the same id, so the server cannot tell them apart. The id is built from the route order and the input mappings, and this almost never happens. Change the route order of one batch, or split it into two batches.

rpc-batch-mapper-missingRuntimeError
Batch mapper `{mapper}` has no generated pure function in this program; the server build cannot register it.

Each inline inputFrom(source, (value) => ...) mapper becomes a pure function the server build copies next to the batch table. This one produced nothing, so the server would answer the batch with a missing mapper error. Fix the pure function errors (purefn-*) at its inputFrom() call, or use the name of a mapper registered on the server.

inputFrom(user, 'toOrgId')
rpc-batch-mapper-unreadableError
`inputFrom()` mapper is not readable at build time ({reason}); pass an inline arrow function or a string literal mapper name.

The mapper you pass to inputFrom() must be an inline function, or a string with the name of a mapper registered on the server. A function reference, a computed string or a value from a parameter cannot be read at build time.

inputFrom(user, (u) => u.id);
// or the name of a mapper registered on the server
inputFrom(user, 'toUserId');
rpc-batch-router-init-hiddenRuntimeError
The batch table {file} was written, but no module of this program calls `createMionRouter` directly, so nothing imports it; import it by hand in the module that creates the router.

The build adds the batch table import to every module that calls createMionRouter from @mionjs/router, following aliases, namespace imports and local re-export files. It found none, for example because a wrapper from another package creates the router, so the server never loads the table. Import the table yourself in the module that creates the router (path relative to that module), or call createMionRouter from a source file of this project.

import './<genDir>/rpc/batches.generated.js';
rpc-batch-source-not-beforeError
`inputFrom()` reads route `{source}` for route `{target}`, but the source is not in this batch or runs after the target; sources must be listed before the routes they feed.

Routes in a batch run in the order you list them, and inputFrom() can only read a route that already ran. Put the source route in the same batch([...]) call, before the route it feeds.

batch([user, routes.orders.list(inputFrom(user, 'toUserId'))]);

Clients

From a client build that ships the routes it calls, or that imports the API type as a value.

rpc-client-api-unreadableError
The API type at this dispatch site cannot be read as a mion PublicApi ({reason}); bundling needs `PublicApi<typeof routes>`.

When routes are bundled (client.routes: 'bundle', the default), the build reads each called route's handler types, options and middlewares from the client's API type. Only PublicApi<typeof routes>, the type the router exports, has them; a loose RemoteApi, an any, or a member without its compiled types does not. Type the client with PublicApi<typeof routes>.

initClient<PublicApi<typeof routes>>({baseURL});
rpc-client-fetch-not-set-upRuntimeError
This client builds with `client.routes: 'fetch'`, so every call fetches its route's metadata, but it never sets up `useFetchMetadata`: every call fails.

With client.routes: 'fetch', each call asks the server how its route works on first use, through the client half of the metadata middleware. Nothing in this program sets that up. Set it up once next to initClient, or build with client.routes: 'bundle' (the default) so every call is bundled.

useFetchMetadata(middlewares.mionFetchMetadata);
rpc-client-imports-server-valueRuntimeError
`{name}` is passed to `initClient` but imported as a value from "{module}", which can put that server module in the client bundle; use `import type`.

The API type you pass to initClient comes from your server code. Imported without type, your bundler can ship that server module, the mion router and everything they import to the browser. It is an error even when your build would drop the import, because a client never imports server code. import type is always removed at build time.

import type {MyApi} from '../server/api';
rpc-client-middleware-not-set-upRuntimeError
The route `{route}` runs the middleware `{middleware}`, which needs params, but this client never sets it up; every call to the route fails its validation.

A middleware gets its params on the client from its onRequest hook. The build found no use of this middleware in the client program, neither a hook nor an installer, so every call sends nothing and the middleware rejects it. Set it up once next to initClient with middlewares.<name>.onRequest(...), or pass it to the installer it ships with.

middlewares.trace.onRequest((trace) => trace({headers: {'X-Trace-Id': crypto.randomUUID()}}));
rpc-client-no-metadata-routeRuntimeError
This client fetches route metadata, but the API it calls does not place `mionFetchMetadata`, so every fetch fails.

A call fetches its metadata when the build could not bundle it, or for every route when client.routes is 'fetch'. The server answers those fetches only through the mionFetchMetadata middleware from @mionjs/router/middlewares, and this API does not add it. Add it first in the server's routes and set up its client half with useFetchMetadata. Or, with bundling on, remove useFetchMetadata and keep every route id a literal so every call is bundled.

// server
mion.initRoutes({mionFetchMetadata, ...routes});
// client
useFetchMetadata(middlewares.mionFetchMetadata);
rpc-client-no-server-versionWarning
The API types this client reads carry no server build version, so a client and server built with different ids are only caught at runtime.

The API types this client reads come from a .d.ts without a server build version, for example one written by plain tsc. The client still builds, but if its ids differ from the server's, you only find out at runtime. Build the API package with mion compile so its types carry the version and the client build checks it.

mion compile --tsconfig tsconfig.build.json
rpc-client-option-widenedWarning
Option `{option}` of `{route}` is not a literal on the API type, so the bundled metadata leaves it unset.

The bundled metadata copies each route's options from the API type, which holds only literal values. A value computed at runtime, like a variable or a call, is left unset on the client, while the server may set it. Write the option as a literal on the route or the router.

mion.route(handler, {sanitizeParams: true})
rpc-client-optional-middleware-not-set-upRuntimeError
The route `{route}` runs the middleware `{middleware}`, but this client never sets it up, so the middleware never gets its params.

All of this middleware's params are optional, so calls still go out, but the middleware never gets anything from this client. The build found no use of it in the client program, neither an onRequest hook nor an installer. Set it up with middlewares.<name>.onRequest(...) or its installer. A middleware with no params at all needs no setup. If sending nothing is on purpose, add // @mion-expect-error rpc-client-optional-middleware-not-set-up above the call.

middlewares.trace.onRequest((trace) => trace({headers: {'X-Trace-Id': crypto.randomUUID()}}));
rpc-client-route-id-widenedRuntimeError
The route id at this call is `string` (a generic helper erased it), so nothing is bundled for it, and this client never sets up `useFetchMetadata`: the call fails.

A helper typed with RouteSubRequest<any> widens the route id to string, so the build cannot bundle the call inside it, and with no useFetchMetadata set up that call fails. Keep the route id through a generic helper, or move the call out of it. Or set up useFetchMetadata so the call asks the server for the route. That builds functions at runtime, which a strict Content Security Policy blocks.

function run<S extends RouteSubRequest<any>>(sub: S) { return sub.call() }
// or fetch the route from the server
useFetchMetadata(middlewares.mionFetchMetadata);
rpc-client-route-id-widened-fetchedInfo
The route id at this call is `string` (a generic helper erased it); the call fetches its metadata from the server instead of using the bundle.

A helper typed with RouteSubRequest<any> widens the route id to string, so the build cannot bundle the call inside it. The call still works because the client sets up useFetchMetadata: it fetches the route's metadata on first use and builds its functions at runtime. To bundle it too, keep the route id through a generic helper.

function run<S extends RouteSubRequest<any>>(sub: S) { return sub.call() }
rpc-client-route-not-declaredError
This call names the route `{route}`, which the API type does not declare; nothing is bundled for it.

The API type has no route with this id, usually because of a route id written by hand or a stale declaration file. Fix the route id, or rebuild the API's declarations.

rpc-client-server-version-mismatchRuntimeError
This client hashes its API ids to the build version {clientVersion}, but the API types it reads come from a server build with {serverVersion}: every call reports a version mismatch.

A server built with mion compile writes its build version into the API type of its .d.ts. This client computed different ids from those types, usually because its tsconfig or a library version differs from the server's, so the server would answer every call with a version mismatch. Build the client with the server's tsconfig and library versions, or use client: {routes: 'fetch'} to read the routes from the server at runtime.

mionVitePlugin({client: {routes: 'fetch'}})
rpc-client-shared-modulesWarning
This program builds with moduleMode `allSingle`, one module per family for the whole program, so the client bundle carries every server type.

In default mode each source file gets its own module of compiled types, so a client bundle only holds the types of the files it imports. allSingle puts every type of the program in one module per family, server types included, and the client bundle loads all of them. Use the default mode for an app with a client and a server.

runTypes: {moduleMode: 'default'}
rpc-client-types-not-built-by-mionRuntimeError
The API package {package} ships types only, but {reason}: a client can only trust types-only packages that `mion api-types` built.

A package with types and no JavaScript entry is read as a types-only API package. mion api-types writes one with a mion.apiTypes field in its package.json and the mion-api.json marker it names. This package lacks them, so its .d.ts may come from another tool, and the client cannot trust its ids, its private fields or its pure functions. Rebuild the package with mion api-types and publish its output folder.

mion api-types --tsconfig tsconfig.json --out api-types
rpc-client-types-other-mion-versionWarning
The API package {package} was built by mion {builtWith}, this build runs {running}: the ids of both sides may differ.

Type ids include the mion version that computed them, so different versions can produce incompatible ids. The client reports a mismatch after reading the server response; fetched route metadata can restore compatible route types. Build the client with the mion version the types package names, or rebuild the package with yours.

npm install -D @mionjs/bin-compiler@<the version in mion-api.json>
rpc-client-version-mismatchRuntimeError
This client injects the build version {clientVersion} but the API in the same program injects {apiVersion}; the client reports a version mismatch against its own server.

initClient and initRoutes each get a build version from the routes they are typed with. In one program they must match, so a difference means the client was typed with another API than the one the router registered. Type the client with PublicApi<typeof routes> from that router.

initClient<PublicApi<typeof routes>>({baseURL});

Enrichment files

From mion enrich --no-emit and the lint rules over generated FriendlyText and MockData files.

enrich-mirror-movedWarning
Mirror location drift: the source maps to `{expected}` but this file lives at `{actual}`; re-run `mion enrich` to relocate.

Each source file has one expected path per file kind under the enrich folder (friendly/, mock/, and one per translation locale). This file sits somewhere else, usually because you moved the source or changed genDir. Run mion enrich <source.ts> <Type> --update to write the files at the right paths.

mion enrich <source.ts> <Type> --update
enrich-mirror-source-missingRuntimeError
Breadcrumb source `{source}` no longer exists ({reason}): the mirror is orphaned; delete it or re-run `mion enrich`.

The import type line at the top of this file points to a source file that no longer exists, so its consts describe types that are gone. If you deleted the source, delete this file, its FriendlyText or MockData sibling, and any translation files. If you moved the source, run mion enrich on it at the new location, then delete the old files.

enrich-mirror-type-missingRuntimeError
Source {source} no longer declares type `{type}`; re-run `mion enrich`.

This file imports a type your source file no longer declares (renamed or removed). Run mion enrich <source.ts> <Type> --update: it comments out the old consts as @rtOrphan, so your values stay. Then run mion enrich --prune to remove the ones that should not come back.

mion enrich <source.ts> <Type> --update
mion enrich --prune
enrich-mirror-unreadableError
Cannot read enrichment mirror file: {reason}

mion could not read this FriendlyText or MockData file, for example because of file permissions, a broken symlink, or another program writing to it at the same moment. Make the file readable, then run mion enrich --no-emit again.

mion enrich --no-emit
enrich-mock-blank-valueWarning
Unfilled blank value: a scaffolded sample pool or range is still empty; fill in real data.

An empty pool (pool: []) is a generated blank, so it mocks nothing. Deleting the @todo line alone is not enough: add realistic sample data. mion enrich --no-emit reports it without failing, while --require-complete and production builds fail on it.

export const mockUser: MockData<User> = {
  name: {pool: ['Ada Lovelace', 'Linus Torvalds']},
};
enrich-mock-orphan-fieldWarning
Stale `@rtOrphanChild` field carcass; run `mion enrich --prune` to remove it (or restore the field).

mion enrich --update (or the build plugin in dev) commented out this field because your type no longer has it. The comment keeps your value. If the field is gone, run mion enrich --prune. If it was renamed, run mion enrich <source.ts> <Type> --update, and the value moves to the new field when the field's type did not change.

# the field is gone
mion enrich --prune
# the field was renamed
mion enrich <source.ts> <Type> --update
enrich-mock-orphan-typeWarning
Stale `@rtOrphan` carcass; run `mion enrich --prune` to remove it (or restore the type).

mion enrich --update (or the build plugin in dev) commented out this MockData const because its type was deleted or renamed. The comment keeps your pools and ranges. If the type is gone, run mion enrich --prune to remove it. If it was renamed, run mion enrich <source.ts> <NewName> --update to get your values back.

# the type is gone
mion enrich --prune
# the type was renamed
mion enrich <source.ts> <NewName> --update
enrich-mock-reserved-prefixError
Property `{property}` collides with the reserved `rt$` enrichment prefix: the type cannot be enriched.

Keys starting with rt$ (rt$items, rt$length, rt$optional, and so on) are reserved for enrichment settings, so mion enrich refuses a type with such a property and writes no MockData file. Rename the property; a plain $ prefix is fine.

interface Config {
  $size: number;
}
enrich-mock-todo-leftWarning
Unfilled `@todo` placeholder; fill in the real sample pools/ranges, then delete the `@todo` line.

mion enrich adds a @todo line to every new MockData const it generates, and the const still holds empty pools. Fill in realistic sample pools and ranges, then delete the @todo line yourself (mion never removes it). mion enrich --no-emit reports it without failing, while --require-complete and production builds fail on it.

/** @rtType User#a1b2c3 @rtIds {name: d4e5f6} */
export const mockUser: MockData<User> = {
  name: {pool: ['Ada Lovelace', 'Linus Torvalds']},
};
enrich-mock-unknown-fieldWarning
Unknown field `{field}`: the type does not declare it, so this MockData entry is dead.

This MockData entry names a field your type does not have: it was removed, renamed or misspelled. Its pool or range never feeds a mock. Remove the entry, or run mion enrich <source.ts> <Type> --update so the file follows the type.

mion enrich <source.ts> <Type> --update
enrich-text-blank-valueWarning
Unfilled blank value: a scaffolded label or message is still empty; fill in the real text.

An empty string ('') in an rt$label or rt$errors slot is a generated blank, and it shows up empty in your UI. Deleting the @todo line alone is not enough: write the real label or message. mion enrich --no-emit reports it without failing, while --require-complete and production builds fail on it.

export const friendlyUser: FriendlyText<User> = {
  name: {rt$label: 'Name'},
};
enrich-text-default-and-messagesWarning
`rt$default` is mutually exclusive with per-constraint messages; use one mode or the other.

An rt$errors object holds either one rt$default message for every failure, or one message per constraint. Mixing both leaves it unclear which message is shown. Keep rt$default alone, or keep the per-constraint keys and remove rt$default.

rt$errors: {
  minLength: 'Name is too short',
}
enrich-text-missing-messageWarning
Error key `{key}` has no message: this failure shows a generic one.

Each failure a field can produce needs its own rt$errors key, unless the record uses rt$default. A missing key shows a generic message for that failure. Add the key, or run mion enrich <source.ts> <Type> --update to add it as a blank to fill in.

mion enrich <source.ts> <Type> --update
enrich-text-orphan-fieldWarning
Stale `@rtOrphanChild` field carcass; run `mion enrich --prune` to remove it (or restore the field).

mion enrich --update (or the build plugin in dev) commented out this field because your type no longer has it. The comment keeps your value. If the field is gone, run mion enrich --prune. If it was renamed, run mion enrich <source.ts> <Type> --update, and the value moves to the new field when the field's type did not change.

# the field is gone
mion enrich --prune
# the field was renamed
mion enrich <source.ts> <Type> --update
enrich-text-orphan-typeWarning
Stale `@rtOrphan` carcass; run `mion enrich --prune` to remove it (or restore the type).

mion enrich --update (or the build plugin in dev) commented out this FriendlyText const because its type was deleted or renamed. The comment keeps your labels and messages. If the type is gone, run mion enrich --prune to remove it. If it was renamed, run mion enrich <source.ts> <NewName> --update to get your values back.

# the type is gone
mion enrich --prune
# the type was renamed
mion enrich <source.ts> <NewName> --update
enrich-text-plural-missing-otherWarning
Plural error template is missing the mandatory `other` arm: the render has no backstop.

A plural message shows the arm that matches the count, and every language falls back to other. Without other, some counts get no message at all. Add an other arm to the plural object.

rt$errors: {
  minLength: {
    one: 'Needs one more character',
    other: 'Needs $[val] more characters',
  },
}
enrich-text-plural-without-countInfo
Constraint `{constraint}` carries no count: a plural template here has dead arms; use a plain string.

Only constraints with a count (minLength, maxLength, min, max, and so on) can pick a plural arm. On any other constraint, such as pattern, only the other arm is ever shown. Replace the plural object with a plain string.

rt$errors: {
  pattern: 'Only letters and numbers are allowed',
}
enrich-text-reserved-prefixError
Property `{property}` collides with the reserved `rt$` enrichment prefix: the type cannot be enriched.

Keys starting with rt$ (rt$label, rt$errors, rt$items, and so on) are reserved for enrichment settings, so mion enrich refuses a type with such a property and writes no FriendlyText file. Rename the property; a plain $ prefix is fine.

interface Config {
  $mode: string;
}
enrich-text-todo-leftWarning
Unfilled `@todo` placeholder; fill in the real labels/messages, then delete the `@todo` line.

mion enrich adds a @todo line to every new FriendlyText const it generates, and the const still holds empty values. Fill in the real labels and error messages, then delete the @todo line yourself (mion never removes it). mion enrich --no-emit reports it without failing, while --require-complete and production builds fail on it.

/** @rtType User#a1b2c3 @rtIds {name: d4e5f6} */
export const friendlyUser: FriendlyText<User> = {
  name: {rt$label: 'Name'},
};
enrich-text-unknown-error-keyWarning
Error key `{key}` is not a declared constraint of this field: the message can never fire.

An rt$errors key must name a failure the field can produce: type, rt$default, or one of the field's format constraints (minLength, pattern, min, and so on). Any other key is never used. Remove the key, or add the matching constraint to the field's type format.

interface User { name: string & FormatString<{minLength: 2}> }
export const friendlyUser: FriendlyText<User> = {
  name: {
    rt$errors: {
      minLength: 'Name needs at least 2 characters',
    },
  },
};
enrich-text-unknown-fieldWarning
Unknown field `{field}`: the type does not declare it, so this FriendlyText entry is dead.

This FriendlyText entry names a field your type does not have: it was removed, renamed or misspelled. Its labels and messages are never used. Remove the entry, or run mion enrich <source.ts> <Type> --update so the file follows the type (a renamed field keeps its values).

mion enrich <source.ts> <Type> --update
enrich-text-unknown-placeholderWarning
Unknown placeholder `$[{placeholder}]`: expected one of `$[label]`, `$[val]`, `$[path]`, `$[index]`.

Error messages fill in only the four placeholders listed above; any other $[name] shows as plain text. A format suffix such as $[val:currency] is not supported either, since plain $[val] already formats by the field's type. Use a supported placeholder, or drop the $[...] wrapper.

rt$errors: {minLength: '$[label] is too short'}
enrich-text-unknown-plural-armWarning
Unknown plural arm `{arm}`: CLDR categories are `zero`, `one`, `two`, `few`, `many`, `other`.

Plural arm names must be CLDR plural categories. Any other name is never picked, in any language. Rename the arm to one of the six categories, or remove it.

rt$errors: {
  minLength: {
    one: 'Needs one more character',
    other: 'Needs $[val] more characters',
  },
}

Internal errors

A bug in the build itself. Please file an issue.

internal-json-primitive-missingRuntimeErrorInternal error
Internal error: JSON composite `{composite}` references primitive entry `{primitive}` (type `{type}`) which was never rendered; please file an issue.

The JSON code mion generated for your type points to a piece that was never built, so it would crash when called and a production build stops. This is a bug in mion, not in your code: report it with the type and the call site named in the error.

internal-kind-not-compilableRuntimeErrorInternal error
Internal error: type `{type}` cannot be compiled here and has no diagnostic, so the function always throws; please file an issue.

The code mion generated for your type meets a kind of value it cannot handle and has no message for, so the function always throws. This is a bug in mion, not in your code: report it with the type and the call site named in the error.

Copyright © 2026