mion provides an ESLint plugin (@mionkit/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 |
npm install @mionkit/eslint-plugin -D
Add the plugin to your ESLint configuration:
{
"plugins": ["@mionkit"],
"rules": {
"@mionkit/strong-typed-routes": "error",
"@mionkit/no-type-imports": "error",
"@mionkit/no-unreachable-union-types": "error",
"@mionkit/no-typeof-runtype": "error",
"@mionkit/pure-functions": "error",
"@mionkit/type-formats-imports": "error"
}
}
Or use the recommended configuration:
{
"extends": ["plugin:@mionkit/recommended"]
}
@mionkit/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
@mionkit/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 '@mionkit/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 '@mionkit/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);
});
@mionkit/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 '@mionkit/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 '@mionkit/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
@mionkit/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'}));
@mionkit/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.
@mionkit/type-formats-importsPrevents using type-only imports (import type { X } or import { type X }) for TypeFormat types from @mionkit/type-formats and @mionkit/run-types.
TypeFormat types (like StrEmail, NumInteger, BigNumFormat, 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 Deepkit reflection
import {StrEmail, StrUrl, StrDate} from '@mionkit/type-formats/FormatsString';
import {NumFormat, NumInteger} from '@mionkit/type-formats/FormatsNumber';
import {BigNumFormat} from '@mionkit/type-formats/FormatsBigint';
import {TypeFormat} from '@mionkit/run-types';
// ❌ WRONG: Type-only imports strip metadata, causing silent validation failures
import type {StrEmail, StrDate} from '@mionkit/type-formats/FormatsString';
import type {NumFloat} from '@mionkit/type-formats/FormatsNumber';
import {type BigNumInt64} from '@mionkit/type-formats/FormatsBigint';
import type {TypeFormat} from '@mionkit/run-types';
plugin:@mionkit/recommended configuration and are enabled by default with error severity.