Devtools

ESLint Rules

ESLint rules specific for mion that ensure RunTypes works properly.

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.

Summary

RuleDescriptionRecommended
strong-typed-routesEnsures handlers have explicit type annotations✅ Error
no-type-importsPrevents type-only imports for route/middleFn types✅ Error
no-typeof-runtypePrevents typeof with runType functions✅ Error
no-unreachable-union-typesDetects unreachable union type members✅ Error
pure-functionsValidates purity of functions passed to pureServerFn and registerPureFnFactory✅ Error
type-formats-importsPrevents type-only imports for TypeFormat types✅ Error

Installation

npm install @mionkit/eslint-plugin -D

Configuration

Add the plugin to your ESLint configuration:

.eslintrc.json
{
  "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:

.eslintrc.json
{
  "extends": ["plugin:@mionkit/recommended"]
}

Rules

@mionkit/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

@mionkit/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 '@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);
});

❌ Invalid Example

// ❌ 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-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 '@mionkit/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 '@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-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'}));

@mionkit/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.


@mionkit/type-formats-imports

Prevents using type-only imports (import type { X } or import { type X }) for TypeFormat types from @mionkit/type-formats and @mionkit/run-types.

Why is this important?

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.

✅ Valid Example

// ✅ 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';

❌ Invalid Example

// ❌ 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';

These rules are included in the plugin:@mionkit/recommended configuration and are enabled by default with error severity.