mion provides an ESLint plugin (@mionjs/eslint-plugin) with rules specifically designed to catch common mistakes and enforce best practices when working with RunTYpes mion routes and middleFns.
| Rule | Description | Recommended |
|---|---|---|
strong-typed-routes | Ensures handlers have explicit type annotations | ✅ Error |
no-type-imports | Prevents type-only imports for route/middleFn types | ✅ Error |
no-typeof-runtype | Prevents typeof with runType functions | ✅ Error |
no-unreachable-union-types | Detects unreachable union type members | ✅ Error |
pure-functions | Validates purity of functions passed to pureServerFn and registerPureFnFactory | ✅ Error |
type-formats-imports | Prevents type-only imports for TypeFormat types | ✅ Error |
no-vite-client | Enforces constraints for non-Vite client builds (Next.js, Turbopack, etc.) | Opt-in |
npm install @mionjs/eslint-plugin -D
Add the plugin to your ESLint configuration:
{
"plugins": ["@mionjs"],
"rules": {
"@mionjs/strong-typed-routes": "error",
"@mionjs/no-type-imports": "error",
"@mionjs/no-unreachable-union-types": "error",
"@mionjs/no-typeof-runtype": "error",
"@mionjs/pure-functions": "error",
"@mionjs/type-formats-imports": "error"
}
}
Or use the recommended configuration:
{
"extends": ["plugin:@mionjs/recommended"]
}
@mionjs/strong-typed-routesEnsures that all route and middleFn handlers have explicit type annotations for parameters and return types. This is essential for mion's automatic validation and serialization to work correctly.
mion uses TypeScript types at runtime to generate validation and serialization functions. Without explicit types, mion cannot properly validate incoming data or serialize responses.
Inline handlers with explicit types:
// 1. Direct inline handlers with proper types
route((ctx, name: string): string => `hello ${name}`);
middleFn((ctx, data: number): void => {
console.log(data);
});
headersFn((c: CallContext, {headers}: HeadersSubset<'auth'>): void => {
// do something
});
Function references with types:
// 2. Function references with proper types
function validHandler(ctx, name: string): string {
return `hello ${name}`;
}
const validArrowHandler = (ctx, name: string): string => `hello ${name}`;
route(validHandler);
route(validArrowHandler);
Using type annotations:
// 3. Type annotations
const typedHandler: Handler = (ctx, name: string): string => `hello ${name}`;
const typedHeaderHandler: HeaderHandler = (c: CallContext, {headers}: HeadersSubset<'auth'>): void => {
const token = headers.auth;
console.log(token);
};
Using satisfies expressions:
// 4. Satisfies expressions
const satisfiesHandler = ((ctx, name: string): string => `hello ${name}`) satisfies Handler;
const satisfiesHeaderHandler = ((c: CallContext, {headers}: HeadersSubset<'auth'>): void => {
const token = headers.auth;
console.log(token);
}) satisfies HeaderHandler;
Using JSDoc tags:
// 5. JSDoc tags
/**
* @mion:route
*/
function routeWithJSDoc(ctx, name: string): string {
return `hello ${name}`;
}
/**
* @mion:middleFn
*/
const middleFnWithJSDoc = (ctx, data: number): void => {
console.log(data);
};
/**
* @mion:headersFn
*/
function headersFnWithJSDoc(c: CallContext, {headers}: HeadersSubset<'auth'>): void {
const token = headers.auth;
console.log(token);
}
Missing types in inline handlers:
// 1. Direct inline handlers missing types
route((ctx, name) => `hello ${name}`); // Missing both param type and return type
middleFn((ctx, data: number) => {
console.log(data);
}); // Missing return type
headersFn((c: CallContext, [token]): void => {
// do something
}); // Missing param type
Missing types in function references:
// 2. Function references missing types
function invalidHandler(ctx, name) {
return `hello ${name}`;
}
const invalidArrowHandler = (ctx, name) => `hello ${name}`;
route(invalidHandler); // Should error: missing both types
route(invalidArrowHandler); // Should error: missing both types
@mionjs/no-type-importsPrevents using type-only imports (import type { X } or import { type X }) for types that are used in route/middleFn parameters or return types.
Type-only imports are completely erased at compile time. When mion needs to generate validation and serialization functions at runtime, it requires the type metadata to be present. If you use import type, the type information is not available at runtime, and mion cannot properly validate or serialize the data.
// ✅ CORRECT: Regular import - types are available at runtime
import {User, Product} from './types.ts';
import {route, middleFn} from '@mionjs/router';
// Types imported without 'type' keyword work correctly with mion
const getUser = route((ctx, id: number): User => {
return {id, name: 'John', email: 'john@example.com'};
});
const createProduct = route((ctx, product: Product): Product => {
return product;
});
const logUser = middleFn((ctx, user: User): void => {
console.log(user.name);
});
// ❌ WRONG: Type-only import - types are erased at runtime
import type {User, Product} from './types.ts';
import {route, middleFn} from '@mionjs/router';
// Types imported with 'type' keyword are erased at runtime
// mion cannot generate validation/serialization functions for them
const getUser = route((ctx, id: number): User => {
return {id, name: 'John', email: 'john@example.com'};
});
const createProduct = route((ctx, product: Product): Product => {
return product;
});
const logUser = middleFn((ctx, user: User): void => {
console.log(user.name);
});
@mionjs/no-typeof-runtypePrevents using typeof when generating a RunType. Using typeof with runtime values can lead to incorrect type inference.
When you use typeof with a runtime value, TypeScript infers the type from the current value, which may not represent all possible values. This can lead to validation that's too strict or serialization that doesn't handle all cases.
import {runType} from '@mionjs/run-types';
// Define the type explicitly
type User = {
id: string;
name: string;
email: string;
};
// Use the explicit type
const userRunType = runType<User>();
import {runType} from '@mionjs/run-types';
// Don't use typeof with runtime values
const user = {id: '1', name: 'John', email: 'john@example.com'};
const userRunType = runType<typeof user>(); // ❌ Error: Don't use typeof
@mionjs/no-unreachable-union-typesDetects union types where some members can never be matched because a less specific type appears earlier in the union. This is particularly important for mion because it affects how data is validated and deserialized.
When mion deserializes incoming JSON data, it tries to match against union types in order. If a less specific type (with fewer properties) comes before a more specific type (with more properties), the more specific type will never be matched because the less specific type will always match first.
Union types with proper order (most specific first):
// 6. Union types with proper order (more specific types first)
type UserResponse = {id: string; name: string; email: string} | {id: string; name: string} | {id: string};
route((ctx): UserResponse => ({id: '1', name: 'John', email: 'john@example.com'}));
// 7. Union types in parameters with proper order
type UserInput = {id: string; name: string; email: string} | {id: string; name: string} | {id: string};
route((ctx, user: UserInput): string => user.id);
Union types with distinct properties:
// 8. Union types with distinct properties (no overlap)
type Action = {type: 'create'; data: string} | {type: 'update'; id: string} | {type: 'delete'; id: string};
route((ctx): Action => ({type: 'create', data: 'test'}));
// 9. Return objects matching single union type (no mixed properties)
type Result = {success: true; data: string} | {success: false; error: string};
route((ctx): Result => ({success: true, data: 'ok'}));
route((ctx): Result => ({success: false, error: 'failed'}));
Subset before superset (unreachable types):
// 1. Unreachable union type in return (subset before superset)
type UnreachableReturn = {a: string} | {a: string; b: number}; // Second type is unreachable
route((ctx): UnreachableReturn => ({a: 'hello'}));
// 2. Unreachable union type in parameter
type UnreachableParam = {id: string} | {id: string; name: string}; // Second type is unreachable
route((ctx, data: UnreachableParam): string => data.id);
Optional properties blocking more specific types:
// 3. Optional properties blocking more specific types
type OptionalBlocking = {a?: string} | {a: string; b: number}; // Second type is unreachable
route((ctx): OptionalBlocking => ({a: 'hello', b: 1}));
// 4. Mixed optional/required blocking
type MixedBlocking = {a: string; b?: number} | {a: string; b: number}; // Second type is unreachable
route((ctx): MixedBlocking => ({a: 'hello', b: 1}));
Multiple unreachable types:
// 5. Multiple unreachable types
type MultipleUnreachable = {a: string} | {a: string; b: number} | {a: string; b: number; c: boolean};
// Both second and third types are unreachable
route((ctx): MultipleUnreachable => ({a: 'hello'}));
@mionjs/pure-functionsValidates that functions passed to pureServerFn() and registerPureFnFactory() are pure and do not use forbidden identifiers, closures, or side effects. See the Pure Functions page for detailed documentation on purity rules and examples.
@mionjs/type-formats-importsPrevents using type-only imports (import type { X } or import { type X }) for TypeFormat types from @mionjs/type-formats and @mionjs/run-types.
TypeFormat types (like FormatEmail, FormatInteger, FormatBigInt, TypeFormat, etc.) rely on the type compiler to preserve type metadata for runtime validation and serialization. Using import type strips this metadata, causing silent failures where format validation simply doesn't work.
// ✅ CORRECT: Regular imports preserve type metadata for runtime reflection
import {FormatEmail, FormatUrl, FormatStringDate} from '@mionjs/type-formats/StringFormats';
import {FormatNumber, FormatInteger} from '@mionjs/type-formats/NumberFormats';
import {FormatBigInt} from '@mionjs/type-formats/BigintFormats';
import {TypeFormat} from '@mionjs/run-types';
// ❌ WRONG: Type-only imports strip metadata, causing silent validation failures
import type {FormatEmail, FormatStringDate} from '@mionjs/type-formats/StringFormats';
import type {FormatFloat} from '@mionjs/type-formats/NumberFormats';
import {type FormatBigInt64} from '@mionjs/type-formats/BigintFormats';
import type {TypeFormat} from '@mionjs/run-types';
@mionjs/no-vite-clientEnforces constraints for projects where the client is not built with Vite (e.g., Next.js with Turbopack). When Vite is not available on the client side, the build-time transforms that automatically inject hash identifiers into pureServerFn() and mapFrom() calls cannot run. This rule ensures developers provide explicit name arguments and do not use APIs that require Vite transforms.
This rule is not included in the recommended config — enable it only in projects that don't use Vite for client builds.
pureServerFn() must have a string literal name as the second argumentmapFrom() must have a string literal name as the third argumentregisterPureFnFactory() is not allowed — it requires Vite build-time transforms and cannot work without them{
"plugins": ["@mionjs"],
"rules": {
"@mionjs/no-vite-client": "error"
}
}
import {pureServerFn} from '@mionjs/core';
import {mapFrom} from '@mionjs/client';
// pureServerFn with explicit name
const addOne = pureServerFn((x: number) => x + 1, 'addOne');
// mapFrom with explicit name
const userToId = mapFrom(userSub, (user: any) => user.id, 'userToId');
import {pureServerFn} from '@mionjs/core';
import {mapFrom} from '@mionjs/client';
import {registerPureFnFactory} from '@mionjs/core';
// ❌ Missing name argument
pureServerFn((x: number) => x + 1);
// ❌ Missing name argument
mapFrom(userSub, (user: any) => user.id);
// ❌ Name must be a string literal, not a variable
const name = 'addOne';
pureServerFn((x: number) => x + 1, name);
// ❌ registerPureFnFactory requires Vite transforms
registerPureFnFactory('ns', 'id', (jitUtils) => (v) => v);
no-vite-client are included in the plugin:@mionjs/recommended configuration and are enabled by default with error severity. The no-vite-client rule is opt-in for projects that don't use Vite for client builds.