DEV Community

Fedar Haponenka
Fedar Haponenka

Posted on

Making AI Code Consistent with Linters

As AI coding assistants like GitHub Copilot, Cursor, and Claude Code become popular, a surprising pattern has emerged: teams that enforce strict linting and code style rules are getting better results from AI agents. The reason? Consistent, predictable code patterns create a perfect environment for AI to contribute safely and effectively.

The Problem: AI's Creative Inconsistency

Ask an AI to implement a new feature, and you'll often get code that's functionally correct but stylistically chaotic:

// ❌ AI without guidance produces inconsistent patterns
async function getUserData(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return await response.json();
}

function getUserData(id: string) {
  return fetch(`/api/users/${id}`).then(res => res.json());
}

const getUserData = async (id: string) => {
  return fetch(`/api/users/${id}`).then(res => res.json());
}
Enter fullscreen mode Exit fullscreen mode

Three similar operations, three different patterns. This inconsistency creates technical debt from day one.

The Limitations of Documentation Alone

Many teams try to guide AI with documentation files like instructions.md, agents.md, or coding-standards.md. While these are valuable for human developers, they're fundamentally suggestions, not guarantees. AI agents might read them, but there's no enforcement mechanism:

# instructions.md - AI might or might not follow these

- Use async/await instead of .then()
- Always add explicit return types
- Use interfaces over type aliases
- Prefer arrow functions for callbacks

# Result: AI sometimes follows, sometimes doesn't
Enter fullscreen mode Exit fullscreen mode

Documentation provides hints, but linters provide rules. The difference is critical: AI can choose to ignore documentation, but it cannot ignore linting errors in your CI pipeline.

The Solution: Teaching AI Your Code Standards

When you configure strict linting rules, you're essentially creating a style guide that AI can follow perfectly. Consider this TypeScript setup:

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/consistent-type-definitions": ["error", "interface"],
    "@typescript-eslint/consistent-type-imports": "error",
    "@typescript-eslint/array-type": ["error", { "default": "generic" }],
    "arrow-body-style": ["error", "always"],
    "prefer-arrow-callback": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

With these rules, AI-generated code becomes predictably consistent:

// ✅ AI with linting rules produces consistent code
interface User {
  id: string;
  name: string;
  email: string;
}

const getUserData = async (userId: string): Promise<User> => {
  const response = await fetch(`/api/users/${userId}`);
  const data: unknown = await response.json();

  // Type guard enforced by linting rules
  if (isValidUser(data)) {
    return data;
  }
  throw new Error('Invalid user data');
};

const isValidUser = (data: unknown): data is User => {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data
  );
};
Enter fullscreen mode Exit fullscreen mode

TypeScript Strictness as a guardrail

TypeScript's strict mode combined with ESLint rules prevents AI from taking shortcuts:

// ❌ AI might try this without constraints
const processData = (data: any) => {
  return data.map(item => item.value * 2);
};

// ✅ With strict linting, AI produces safe code
interface DataPoint {
  value: number;
  timestamp: Date;
}

const processData = (dataPoints: DataPoint[]): number[] => {
  return dataPoints.map((dataPoint: DataPoint): number => {
    return dataPoint.value * 2;
  });
};
Enter fullscreen mode Exit fullscreen mode

Import/Export Consistency

AI tends to mix import styles, but linters enforce uniformity:

// ❌ Inconsistent imports AI might generate
import React from 'react';
import { useState, useEffect, useCallback } from "react";
import { type User, getUsers } from '../../services/userService';
import { formatDate, isValidEmail } from '@/utils/helpers';

// ✅ Linter-enforced consistency
import React, { useState, useEffect, useCallback } from 'react';
import { type User, getUsers } from '@/services/userService';
import { formatDate, isValidEmail } from '@/utils/helpers';
Enter fullscreen mode Exit fullscreen mode

Naming Convention Enforcement

Linters make AI use consistent naming across the codebase:

// With naming-convention rules, AI learns your patterns
interface UserPreferences {
  theme: 'light' | 'dark';
  notificationsEnabled: boolean;
  language: string;
}

class UserService {
  private apiClient: ApiClient;

  async getUserPreferences(userId: string): Promise<UserPreferences> {
    return this.apiClient.fetch<UserPreferences>(
      `/users/${userId}/preferences`
    );
  }

  async updateUserPreferences(
    userId: string,
    preferences: Partial<UserPreferences>
  ): Promise<void> {
    await this.apiClient.update(
      `/users/${userId}/preferences`,
      preferences
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The Magic of Prettier + ESLint Integration

When you combine Prettier for formatting with ESLint for rules, AI produces code that looks like it was written by your team:

// .prettierrc.json
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "arrowParens": "always"
}

// AI-generated code with Prettier + ESLint
interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

const handleUserUpdate = async (
  userId: string,
  updates: Partial<User>
): Promise<ApiResponse<User>> => {
  try {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(updates),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();
    return {
      data: result,
      status: response.status,
    };
  } catch (error) {
    console.error('Failed to update user:', error);
    throw error;
  }
};
Enter fullscreen mode Exit fullscreen mode

Different Rules for Humans vs AI

An emerging best practice is to apply stricter rules to AI-generated code than to human-written code. Rules that feel burdensome for developers are trivial for AI to follow perfectly:

Human-friendly linting rules (.eslintrc.json):

{
  "overrides": [
    {
      "files": ["*.ts", "*.tsx"],
      "rules": {
        "require-jsdoc": "off",
        "max-lines-per-function": "off",
        "complexity": ["warn", 10],
        "no-nested-ternary": "warn",
        "@typescript-eslint/explicit-function-return-type": "warn"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

AI-optimized strict rules (.eslintrc.ai.json):

{
  "extends": ["./.eslintrc.json"],
  "overrides": [
    {
      "files": ["*.ai.ts", "*.ai.tsx", "*-generated.ts", "src/ai/**/*.ts"],
      "rules": {
        "require-jsdoc": "error",
        "max-lines-per-function": ["error", { "max": 20 }],
        "complexity": ["error", 5],
        "no-nested-ternary": "error",
        "no-magic-numbers": "error",
        "@typescript-eslint/explicit-function-return-type": "error",
        "@typescript-eslint/explicit-member-accessibility": "error"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This dual-rule approach recognizes the fundamental difference between human and AI coding:

  • Humans need flexibility for creative problem-solving and rapid iteration
  • AI excels at following rigid patterns consistently without cognitive overhead

Practical Implementation

{
  "scripts": {
    "lint:human": "eslint . --ext .ts,.tsx --config .eslintrc.json",
    "lint:ai": "eslint . --ext .ts,.tsx --config .eslintrc.ai.json",
    "lint:all": "npm run lint:human && npm run lint:ai"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create an AI-specific linting workflow:

bash
# Check AI-generated code with strict rules
npm run lint:ai --config .eslintrc.ai.json

# Check human code with developer-friendly rules  
npm run lint:human --config .eslintrc.json

# In CI/CD pipeline
if [ "$AUTHOR" = "AI_ASSISTANT" ]; then
  npm run lint:ai
else
  npm run lint:human
fi
Enter fullscreen mode Exit fullscreen mode

Smart Constraints for Smart Tools

The most successful teams aren't just using AI to write code. They're creating targeted constraints that maximize AI's strengths while minimizing developer friction. By applying stricter linting rules selectively to AI-generated code, you get the benefits of perfect consistency without burdening your human developers.
Your linting configuration is becoming more than just a style guide. It's becoming a collaboration framework that optimizes how humans and AI work together.

Top comments (0)