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:
- Speed โก: Only checks files youโre committing, not the entire codebase
- Team Consistency ๐ฅ: Everyone follows the same standards automatically
- Error Prevention ๐ก๏ธ: Catches issues before they become problems
- Automation ๐ค: No more forgetting to run linters manually
- 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
- โก Keep It Fast: Only run necessary checks on staged files
- ๐ฏ Be Specific: Use file patterns to target the right tools
- ๐ Auto-fix When Possible: Let tools fix issues automatically
- ๐งช Test Smartly: Run related tests, not the entire suite
- ๐ Document Your Setup: Help teammates understand the workflow
- ๐ Fail Fast: Stop on first error to save time
- ๐ง 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:
- ๐ป Set up lint-staged in your current project
- ๐๏ธ Experiment with different configurations for your teamโs needs
- ๐ Learn about GitHub Actions for additional CI/CD checks
- ๐ 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! ๐๐โจ