Source Conversion
Converting Files Between Types and Builders
The convert command rewrites your files in place, from plain TypeScript types to type builders or back.
# one file, or several
npx mion convert --to builders src/models.ts src/api.ts
# or a whole folder (every .ts and .tsx inside it)
npx mion convert --to type src/models/
Declarations already in the target form are left exactly as they are, so running it twice is safe.
// before
export type User = {
id: number;
name?: string;
tags: string[];
};
After --to builders the type name stays as an alias, so every import still works.
// after mion convert --to builders
export const userRT = RT.object({
id: TF.number(),
name: RT.optional(TF.string()),
tags: RT.array(TF.string()),
});
export type UserAsBuilders = InferType<typeof userRT>; // the real command keeps the name User
Converting Types Written Inside a Call
A type written straight into a factory call has no declaration, so the converter rewrites the call.
// before
export const isOrder = createValidateFn<{id: string; total: number}>();
The builder form passes the type to the same factory as a value.
// after: the same shape, so the same validator
export const isOrderBuilt = createValidateFn(
RT.object({id: TF.string(), total: TF.number()})
);
getRunTypeId<{id: string; total: number}>() ===
getRunTypeId(RT.object({id: TF.string(), total: TF.number()})); // true
A call that names its type (createValidateFn<Order>()) is left alone, because its declaration converts on its own. So is a call that reflects a runtime value (createValidateFn(order)): it has no written type to move.
Preserving Type Identity
Conversion changes the spelling, never the type's identity. You get the same validator, codecs and mock data.
getRunTypeId<User>() === getRunTypeId(userRT); // true
Shapes That Convert
Everything RunTypes reflects: primitives and literals, string and number formats, arrays and tuples (readonly included), objects (optional, readonly and quoted keys included), records (readonly included), unions, enums, classes, Date, Map, Set, Promise, RegExp, Temporal types, functions, template literals, branded types, and recursive types. Most shapes the builder form has no word for convert through the getRunType<T>() escape, like functions, template literals and bigint literals.
In builder form, labeled tuples and named function parameters use RT.slot, a readonly array or tuple is wrapped in RT.readonly, an index signature beside named properties becomes an intersection, and recursive shapes come out as RT.circular with RT.self().
Recursive Types
Some recursive types cannot write their loop as builders: one that also needs the getRunType escape (usually for a method member), and one that loops back through its own tuple slot, like type Pair = [number, Pair]. The converter keeps the type, readonly included, and adds a const beside it.
export type TreeNode = {label(): string; kids: TreeNode[]};
export const treeNodeRT = getRunType<TreeNode>();
That pair is the builder form. Running the conversion again leaves it alone, and converting back to the type form removes the const.
References Between Files
A type that names another converted type keeps the name in every form. Convert the files together and the imports are updated for you.
npx mion convert --to builders src/user.ts src/order.ts
Shapes That Do Not Convert
These shapes do not convert, not even through the escape. The converter reports each one, leaves that declaration as written, converts the rest of the file, and exits with a non-zero code.
| Shape | Why |
|---|---|
| A cycle that never passes through a named type | The self reference has nothing to point at. Give the inner type a name and it converts. |
| A symbol-keyed property | The escape can only carry a type it can write down, and a symbol name is not one. |
A unique symbol literal type (typeof sym) | No builder can spell a symbol value. |
A property tagged @nonEnumerable | The tag shapes the wire format but has no builder spelling, so dropping it would change the type's identity. |
A recursive type reaching a call site through a mapped type such as DataOnly<T> | The mapping hands the call an unnamed copy of the loop, so there is no name to close it on. Pass the named type itself instead. |
| A recursive tuple reaching a call site through a mapped type | Same cause, and a tuple slot cannot close the loop. Put the recursion behind an object, array, Map, Set or function slot. |
| A reference to a type whose file is not in the same run | Add that file to the same command. |
A Temporal type that resolves to any | The Temporal declarations are missing from your project. Add the ESNext.Temporal lib or a polyfill's types. Converting would make the any permanent. |
A generic type (type Box<T> = ...) is not an error: a type parameter has no runtime shape. The declaration is reported and left as written, and every use with real types converts normally.
With --to type, a builder const your code still uses elsewhere is also reported and left as written.
@mionjs/run-types or a @mionjs/drizzle-orm package is an error (convert-unresolved-import) that names the package. The declarations that use it stay as written and the run exits with a non-zero code. Install or build the package, or check paths and customConditions in your tsconfig.Dry Runs
--check reports what would change without writing, and exits with code 1 if anything would change.
npx mion convert --to type --check src/models/
Output Folders
--out-dir writes the converted files into a copy of the folder and leaves your sources untouched.
npx mion convert --to builders src/models/ --out-dir converted/