ESLint Rules
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.
Summary
| 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 |
Installation
npm install @mionjs/eslint-plugin -D
Configuration
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"]
}
Rules
@mionjs/strong-typed-routes
Ensures 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.
Why is this important?
mion uses TypeScript types at runtime to generate validation and serialization functions. Without explicit types, mion cannot properly validate incoming data or serialize responses.
✅ Valid Examples
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);
}
❌ Invalid Examples
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-imports
Prevents using type-only imports (import type { X } or import { type X }) for types that are used in route/middleFn parameters or return types.
Why is this important?
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.
✅ Valid Example
// ✅ 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);
});
❌ Invalid Example
// ❌ 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-runtype
Prevents using typeof when generating a RunType. Using typeof with runtime values can lead to incorrect type inference.
Why is this important?
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.
✅ Valid Example
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>();
❌ Invalid Example
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-types
Detects 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.
Why is this important?
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.
✅ Valid Examples
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'}));
❌ Invalid Examples
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-functions
Validates 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-imports
Prevents using type-only imports (import type { X } or import { type X }) for TypeFormat types from @mionjs/type-formats and @mionjs/run-types.
Why is this important?
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.
✅ Valid Example
// ✅ 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';
❌ Invalid Example
// ❌ 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-client
Enforces 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 serverMapFrom() 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.
What it checks
pureServerFn()must have a string literal name as the second argumentserverMapFrom()must have a string literal name as the third argumentregisterPureFnFactory()is not allowed — it requires Vite build-time transforms and cannot work without them- Name arguments must be string literals, not variables or expressions
Configuration
{
"plugins": ["@mionjs"],
"rules": {
"@mionjs/no-vite-client": "error"
}
}
✅ Valid Examples
import {pureServerFn} from '@mionjs/core';
import {serverMapFrom} from '@mionjs/client';
// pureServerFn with explicit name
const addOne = pureServerFn((x: number) => x + 1, 'addOne');
// serverMapFrom with explicit name
const userToId = serverMapFrom(userSub, (user: any) => user.id, 'userToId');
❌ Invalid Examples
import {pureServerFn} from '@mionjs/core';
import {serverMapFrom} from '@mionjs/client';
import {registerPureFnFactory} from '@mionjs/core';
// ❌ Missing name argument
pureServerFn((x: number) => x + 1);
// ❌ Missing name argument
serverMapFrom(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.