+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 256 of 354

📘 Lint-Staged: Pre-Commit Checks

Master lint-staged: pre-commit checks in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand lint-staged fundamentals 🎯
  • Apply lint-staged in real projects 🏗️
  • Debug common issues 🐛
  • Write type-safe code ✨

🎯 Introduction

Welcome to this exciting tutorial on lint-staged! 🎉 In this guide, we’ll explore how to set up automated pre-commit checks that keep your TypeScript code clean and consistent.

You’ll discover how lint-staged can transform your development workflow by catching issues before they reach your repository. Whether you’re building web applications 🌐, server-side code 🖥️, or libraries 📚, understanding lint-staged is essential for maintaining code quality and team productivity.

By the end of this tutorial, you’ll feel confident setting up automated pre-commit checks in your own projects! Let’s dive in! 🏊‍♂️

📚 Understanding Lint-Staged

🤔 What is Lint-Staged?

Lint-staged is like having a careful editor 📝 who reviews only the pages you’ve changed before you submit your story. Think of it as a quality gate 🚪 that ensures only the files you’re committing meet your project’s standards.

In TypeScript terms, lint-staged runs linting, formatting, and testing scripts only on staged files (the ones you’re about to commit) ⚡. This means you can:

  • ✨ Catch issues before they reach the repository
  • 🚀 Speed up your checks by only running on changed files
  • 🛡️ Maintain consistent code quality across your team
  • 🎯 Prevent broken code from entering your main branch

💡 Why Use Lint-Staged?

Here’s why developers love lint-staged:

  1. Speed ⚡: Only checks files you’re committing, not the entire codebase
  2. Team Consistency 👥: Everyone follows the same standards automatically
  3. Error Prevention 🛡️: Catches issues before they become problems
  4. Automation 🤖: No more forgetting to run linters manually
  5. Flexible Configuration 🔧: Run different tools on different file types

Real-world example: Imagine working on a shopping cart feature 🛒. With lint-staged, when you commit your changes, it automatically runs TypeScript checks, formats your code, and runs tests only on the files you modified!

🔧 Basic Syntax and Usage

📝 Installation

Let’s start by setting up lint-staged in your project:

# 📦 Install lint-staged and husky for git hooks
npm install --save-dev lint-staged husky

# 🎨 Or if you're using pnpm
pnpm add -D lint-staged husky

🎯 Basic Configuration

Here’s how to configure lint-staged in your package.json:

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  },
  "scripts": {
    "prepare": "husky install"
  }
}

💡 Explanation: This configuration runs ESLint and Prettier on TypeScript files, and just Prettier on JSON and Markdown files!

🚀 Setting Up Git Hooks

Create a pre-commit hook to run lint-staged:

# 🎯 Initialize husky
npx husky install

# 🔗 Create pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"

Now your .husky/pre-commit file should contain:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 🎨 Run lint-staged on commit
npx lint-staged

💡 Practical Examples

🛒 Example 1: E-commerce Project Setup

Let’s configure lint-staged for a TypeScript e-commerce project:

{
  "lint-staged": {
    "src/**/*.{ts,tsx}": [
      "eslint --fix --max-warnings 0",
      "prettier --write",
      "git add"
    ],
    "src/**/*.test.{ts,tsx}": [
      "jest --findRelatedTests --passWithNoTests"
    ],
    "*.{json,md,yml,yaml}": [
      "prettier --write"
    ],
    "package.json": [
      "sort-package-json"
    ]
  }
}

🎯 What this does:

  • ✨ Runs ESLint with zero warnings allowed on TypeScript files
  • 🎨 Formats code with Prettier
  • 🧪 Runs related tests for changed files
  • 📄 Formats configuration files
  • 📦 Sorts package.json automatically

🎮 Example 2: Game Development Project

For a TypeScript game project with specific needs:

// 📁 lint-staged.config.js
const path = require('path');

module.exports = {
  // 🎮 Game source files
  'src/game/**/*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    // 🎯 Custom type checking for game files
    () => 'tsc --noEmit --project tsconfig.game.json'
  ],
  
  // 🧪 Test files
  '**/*.{test,spec}.{ts,tsx}': [
    'jest --findRelatedTests --passWithNoTests',
    'eslint --fix',
    'prettier --write'
  ],
  
  // 🎨 Asset configuration
  'src/assets/**/*.json': [
    // 🔍 Validate game asset JSON files
    (filenames) => filenames.map(filename => 
      `node scripts/validate-asset.js ${filename}`
    )
  ],
  
  // 📝 Documentation
  'docs/**/*.md': [
    'markdownlint --fix',
    'prettier --write'
  ]
};

🏦 Example 3: Enterprise Application

For a large TypeScript application with multiple teams:

// 📁 lint-staged.config.js
module.exports = {
  // 🏢 Core business logic
  'src/core/**/*.{ts,tsx}': [
    'eslint --fix --config .eslintrc.core.json',
    'prettier --write',
    // 🛡️ Extra strict type checking for core
    () => 'tsc --noEmit --strict --project tsconfig.core.json'
  ],
  
  // 🌐 Frontend components
  'src/components/**/*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    // 🎨 Run component tests
    'jest --findRelatedTests --passWithNoTests',
    // 📸 Update snapshots if needed
    (filenames) => {
      const testFiles = filenames
        .filter(f => f.includes('.test.') || f.includes('.spec.'))
        .join(' ');
      return testFiles ? `jest --updateSnapshot ${testFiles}` : '';
    }
  ],
  
  // 🔧 Configuration files
  '*.{json,yml,yaml}': [
    'prettier --write',
    // 🔍 Validate configuration syntax
    (filenames) => filenames
      .filter(f => f.includes('config'))
      .map(f => `node scripts/validate-config.js ${f}`)
  ]
};

🚀 Advanced Concepts

🧙‍♂️ Advanced Configuration: Conditional Checks

When you’re ready to level up, try conditional configurations:

// 📁 lint-staged.config.js
const { CLIEngine } = require('eslint');

const cli = new CLIEngine({});

module.exports = {
  '*.{ts,tsx}': (filenames) => {
    // 🎯 Only run on files that aren't ignored by ESLint
    const filteredFiles = filenames.filter(file => !cli.isPathIgnored(file));
    
    const commands = [];
    
    if (filteredFiles.length > 0) {
      // 🔍 Standard checks
      commands.push(`eslint --fix ${filteredFiles.join(' ')}`);
      commands.push(`prettier --write ${filteredFiles.join(' ')}`);
      
      // 🧪 Run tests only if more than 5 files changed
      if (filteredFiles.length > 5) {
        commands.push('npm run test:unit');
      } else {
        commands.push(`jest --findRelatedTests ${filteredFiles.join(' ')}`);
      }
      
      // 🎯 Type check all files if core files changed
      const hasCoreChanges = filteredFiles.some(f => f.includes('src/core/'));
      if (hasCoreChanges) {
        commands.push('tsc --noEmit');
      }
    }
    
    return commands;
  }
};

🏗️ Advanced Workflow: Multiple Environments

For complex projects with different environments:

// 📁 lint-staged.config.js
const isProduction = process.env.NODE_ENV === 'production';

const config = {
  // 🎯 Base configuration for all environments
  '*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write'
  ]
};

if (isProduction) {
  // 🏭 Production-specific checks
  config['*.{ts,tsx}'].push(
    // 🛡️ Strict type checking
    () => 'tsc --noEmit --strict',
    // 📊 Bundle size check
    () => 'npm run build:analyze',
    // 🔒 Security audit
    () => 'npm audit --audit-level moderate'
  );
} else {
  // 🧪 Development-specific checks
  config['*.{ts,tsx}'].push(
    // ⚡ Quick tests only
    'jest --findRelatedTests --passWithNoTests'
  );
}

module.exports = config;

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Slow Pre-commit Hooks

# ❌ Wrong - runs on entire codebase!
"pre-commit": "eslint src/ && prettier --write src/ && jest"

# ✅ Correct - only runs on staged files!
"lint-staged": {
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.test.{ts,tsx}": ["jest --findRelatedTests"]
}

🤯 Pitfall 2: Forgetting to Stage Fixed Files

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
      // ❌ Missing: files won't be staged after fixes!
    ]
  }
}
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
      // ✅ Now fixed files are automatically staged!
    ]
  }
}

🚫 Pitfall 3: Ignoring Exit Codes

// ❌ Dangerous - ignores failures!
module.exports = {
  '*.{ts,tsx}': [
    'eslint --fix || true',  // Don't do this!
    'prettier --write'
  ]
};

// ✅ Safe - proper error handling
module.exports = {
  '*.{ts,tsx}': [
    'eslint --fix',  // Will fail commit if errors found
    'prettier --write'
  ]
};

🛠️ Best Practices

  1. ⚡ Keep It Fast: Only run necessary checks on staged files
  2. 🎯 Be Specific: Use file patterns to target the right tools
  3. 🔄 Auto-fix When Possible: Let tools fix issues automatically
  4. 🧪 Test Smartly: Run related tests, not the entire suite
  5. 📝 Document Your Setup: Help teammates understand the workflow
  6. 🚀 Fail Fast: Stop on first error to save time
  7. 🔧 Use Configs: Keep configuration in dedicated files for complex setups

💡 Pro Tips:

// 🎯 Pro tip: Use functions for dynamic commands
module.exports = {
  '*.{ts,tsx}': (filenames) => [
    `eslint --fix ${filenames.join(' ')}`,
    `prettier --write ${filenames.join(' ')}`,
    // 🧪 Only run tests if test files changed
    ...(filenames.some(f => f.includes('.test.')) 
      ? [`jest --findRelatedTests ${filenames.join(' ')}`] 
      : [])
  ]
};

🧪 Hands-On Exercise

🎯 Challenge: Set Up Complete Pre-commit Workflow

Create a comprehensive lint-staged setup for a TypeScript project:

📋 Requirements:

  • ✅ Format TypeScript files with Prettier
  • 🔍 Lint TypeScript files with ESLint (zero warnings)
  • 🧪 Run tests for changed files
  • 📦 Sort package.json automatically
  • 🔒 Type check the entire project if core files change
  • 📝 Format markdown files
  • 🎨 Validate JSON configuration files

🚀 Bonus Points:

  • Add custom validation for specific file types
  • Implement conditional checks based on file count
  • Add performance monitoring for the pre-commit process

💡 Solution

🔍 Click to see solution
// 📁 lint-staged.config.js
const fs = require('fs');
const path = require('path');

// 🎯 Helper function to validate JSON files
const validateJson = (filenames) => {
  return filenames.map(filename => {
    try {
      const content = fs.readFileSync(filename, 'utf8');
      JSON.parse(content);
      return `echo "✅ ${filename} is valid JSON"`;
    } catch (error) {
      return `echo "❌ ${filename} has invalid JSON: ${error.message}" && exit 1`;
    }
  });
};

// 🚀 Performance monitoring
const timeCommand = (command) => `time ${command}`;

module.exports = {
  // 🎨 TypeScript source files
  'src/**/*.{ts,tsx}': (filenames) => {
    const commands = [
      // 🔧 Fix linting issues
      `eslint --fix --max-warnings 0 ${filenames.join(' ')}`,
      // 🎨 Format code
      `prettier --write ${filenames.join(' ')}`
    ];
    
    // 🧪 Run tests for changed files
    commands.push(`jest --findRelatedTests ${filenames.join(' ')} --passWithNoTests`);
    
    // 🔒 Type check if core files changed
    const hasCoreChanges = filenames.some(f => f.includes('src/core/') || f.includes('src/types/'));
    if (hasCoreChanges) {
      commands.push('echo "🔒 Core files changed, running full type check..."');
      commands.push(timeCommand('tsc --noEmit'));
    }
    
    return commands;
  },
  
  // 🧪 Test files
  '**/*.{test,spec}.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    // 🚀 Run the specific test files
    (filenames) => `jest ${filenames.join(' ')} --passWithNoTests`
  ],
  
  // 📦 Package configuration
  'package.json': [
    'sort-package-json',
    'prettier --write',
    // 🔍 Validate package.json structure
    'node -e "console.log(\'✅ package.json is valid\')"'
  ],
  
  // 🔧 Configuration files
  '*.{json,jsonc}': [
    ...validateJson,
    'prettier --write'
  ],
  
  // 📝 Documentation
  '**/*.md': [
    'markdownlint --fix',
    'prettier --write'
  ],
  
  // 🎨 Style files
  '**/*.{css,scss}': [
    'stylelint --fix',
    'prettier --write'
  ],
  
  // 🌐 Configuration validation
  'config/**/*': [
    // 🔍 Custom validation for config files
    (filenames) => filenames.map(f => `node scripts/validate-config.js ${f}`)
  ]
};
// 📁 package.json scripts section
{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
    "type-check": "tsc --noEmit",
    "test:unit": "jest",
    "test:related": "jest --findRelatedTests",
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    // 🔗 Reference to config file
    "*": "lint-staged --config lint-staged.config.js"
  }
}
#!/usr/bin/env sh
# 📁 .husky/pre-commit

. "$(dirname -- "$0")/_/husky.sh"

echo "🚀 Running pre-commit checks..."

# ⏰ Track execution time
START_TIME=$(date +%s)

# 🎯 Run lint-staged
npx lint-staged

# ⏱️ Show execution time
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "✅ Pre-commit checks completed in ${DURATION}s"

🎓 Key Takeaways

You’ve learned so much! Here’s what you can now do:

  • Set up lint-staged with confidence 💪
  • Configure pre-commit hooks that keep your code clean 🛡️
  • Optimize performance by running checks only on staged files ⚡
  • Debug hook issues like a pro 🐛
  • Maintain code quality automatically across your team! 🚀

Remember: Lint-staged is your quality guardian, not a roadblock! It’s here to help you ship better code faster. 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered lint-staged pre-commit checks!

Here’s what to do next:

  1. 💻 Set up lint-staged in your current project
  2. 🏗️ Experiment with different configurations for your team’s needs
  3. 📚 Learn about GitHub Actions for additional CI/CD checks
  4. 🌟 Share your setup with your team and help them level up!

Remember: Every clean codebase started with good habits. Keep committing quality code, and most importantly, have fun building amazing things! 🚀


Happy coding! 🎉🚀✨