Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
- Git version control knowledge ๐ฒ
What you'll learn
- Understand git hooks and husky fundamentals ๐ฏ
- Apply husky in real TypeScript projects ๐๏ธ
- Debug common husky issues ๐
- Write type-safe pre-commit workflows โจ
๐ฏ Introduction
Welcome to the exciting world of Git hooks with Husky! ๐ In this guide, weโll explore how to automate your development workflow and catch issues before they reach your repository.
Youโll discover how Husky can transform your TypeScript development experience by automatically running tests ๐งช, linters ๐, and type checks โ every time you make a commit. Whether youโre working solo or with a team ๐ฅ, understanding Husky is essential for maintaining code quality and preventing bugs from sneaking into your codebase.
By the end of this tutorial, youโll feel confident setting up robust pre-commit workflows in your TypeScript projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Git Hooks and Husky
๐ค What are Git Hooks?
Git hooks are like automated security guards ๐ก๏ธ for your repository. Think of them as checkpoints at an airport โ๏ธ that ensure everything is safe before letting you board (or in this case, commit your code).
In Git terms, hooks are scripts that run automatically at specific points in your Git workflow ๐. This means you can:
- โจ Run tests before every commit
- ๐ Check code formatting automatically
- ๐ก๏ธ Prevent bad code from entering your repository
- ๐ Enforce commit message standards
๐ก What is Husky?
Husky is like a friendly dog trainer ๐ that makes managing Git hooks super easy! Instead of dealing with complex shell scripts hidden in .git/hooks
, Husky lets you:
- Easy Setup ๐ง: Configure hooks with simple npm commands
- Version Control ๐ฆ: Keep your hooks in your repository
- Team Sharing ๐ฅ: Everyone gets the same quality checks
- TypeScript Integration ๐: Perfect for TypeScript projects
Real-world example: Imagine working on a team project ๐จโ๐ป๐ฉโ๐ป. With Husky, every developer automatically runs the same linting and testing checks before committing, ensuring consistent code quality!
๐ง Basic Installation and Setup
๐ฆ Installing Husky
Letโs start with a friendly setup process:
# ๐ Install husky as a dev dependency
npm install --save-dev husky
# ๐ฏ Initialize husky in your project
npx husky install
# โจ Make husky installation automatic for new contributors
npm pkg set scripts.prepare="husky install"
๐ก Explanation: The prepare
script ensures husky gets set up automatically when someone runs npm install
on your project!
๐จ Project Structure
After installation, your project will look like this:
your-project/
โโโ .husky/ # ๐ Husky configuration folder
โ โโโ _/ # ๐ Husky internal files
โ โโโ pre-commit # ๐ฏ Pre-commit hook (we'll create this)
โโโ package.json # ๐ฆ Dependencies and scripts
โโโ src/ # ๐ป Your TypeScript code
๐ฏ Your First Hook
Letโs create a simple pre-commit hook:
# ๐ ๏ธ Create a pre-commit hook that runs TypeScript checks
npx husky add .husky/pre-commit "npm run typecheck"
This creates a file at .husky/pre-commit
that looks like:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run typecheck
๐ก Practical Examples
๐ Example 1: E-commerce TypeScript Project
Letโs set up comprehensive quality checks for an online store:
// ๐ช Our e-commerce types
interface Product {
id: string;
name: string;
price: number;
category: 'electronics' | 'clothing' | 'books';
inStock: boolean;
emoji: string; // Every product needs personality!
}
interface ShoppingCart {
id: string;
items: CartItem[];
total: number;
customerEmail: string;
}
interface CartItem {
product: Product;
quantity: number;
}
// ๐๏ธ Shopping cart service
class CartService {
private carts: Map<string, ShoppingCart> = new Map();
// โ Add item to cart
addToCart(cartId: string, product: Product, quantity: number): void {
const cart = this.carts.get(cartId);
if (!cart) {
throw new Error(`๐ซ Cart ${cartId} not found!`);
}
if (!product.inStock) {
throw new Error(`๐ฆ ${product.name} is out of stock!`);
}
const existingItem = cart.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.items.push({ product, quantity });
}
this.updateTotal(cart);
console.log(`โ
Added ${product.emoji} ${product.name} to cart!`);
}
// ๐ฐ Calculate total
private updateTotal(cart: ShoppingCart): void {
cart.total = cart.items.reduce((sum, item) =>
sum + (item.product.price * item.quantity), 0
);
}
}
package.json scripts for this project:
{
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src/**/*.ts",
"format": "prettier --write src/**/*.ts",
"test": "jest",
"build": "tsc"
}
}
๐ Husky hooks setup:
# ๐ฏ Pre-commit: Run all quality checks
npx husky add .husky/pre-commit "npm run lint && npm run typecheck && npm run test"
# ๐ Pre-push: Ensure build works
npx husky add .husky/pre-push "npm run build"
# ๐ Commit message validation
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
๐ฎ Example 2: Game Development Project
Letโs make our game development workflow bulletproof:
// ๐ฏ Game state management
interface GameState {
player: Player;
enemies: Enemy[];
score: number;
level: number;
powerUps: PowerUp[];
}
interface Player {
id: string;
name: string;
health: number;
position: Position;
inventory: Item[];
emoji: string;
}
interface Enemy {
id: string;
type: 'goblin' | 'dragon' | 'skeleton';
health: number;
damage: number;
position: Position;
emoji: string;
}
interface Position {
x: number;
y: number;
}
// ๐ฎ Game engine
class GameEngine {
private state: GameState;
constructor(playerName: string) {
this.state = {
player: {
id: 'player1',
name: playerName,
health: 100,
position: { x: 0, y: 0 },
inventory: [],
emoji: '๐งโโ๏ธ'
},
enemies: [],
score: 0,
level: 1,
powerUps: []
};
}
// โ๏ธ Battle logic
attack(targetId: string): boolean {
const enemy = this.state.enemies.find(e => e.id === targetId);
if (!enemy) {
console.log('๐ซ Enemy not found!');
return false;
}
const damage = Math.floor(Math.random() * 20) + 10;
enemy.health -= damage;
console.log(`โ๏ธ Hit ${enemy.emoji} for ${damage} damage!`);
if (enemy.health <= 0) {
this.defeatEnemy(enemy);
return true;
}
return false;
}
// ๐ Victory!
private defeatEnemy(enemy: Enemy): void {
this.state.enemies = this.state.enemies.filter(e => e.id !== enemy.id);
this.state.score += 100;
console.log(`๐ Defeated ${enemy.emoji}! Score: ${this.state.score}`);
}
}
๐ง Advanced Husky Configuration:
# ๐งช Run specific tests for game files
npx husky add .husky/pre-commit "npm run test:game && npm run lint:strict"
# ๐จ Auto-format game assets
npx husky add .husky/pre-commit "npm run format:assets"
# ๐๏ธ Build and test game bundle
npx husky add .husky/pre-push "npm run build:game && npm run test:integration"
๐ Advanced Husky Configurations
๐งโโ๏ธ Conditional Hooks with lint-staged
For better performance, only check changed files:
# ๐ฆ Install lint-staged
npm install --save-dev lint-staged
package.json configuration:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"tsc --noEmit"
],
"*.{json,md}": [
"prettier --write"
]
},
"scripts": {
"pre-commit": "lint-staged"
}
}
๐ Updated husky hook:
# โก Only check changed files (much faster!)
npx husky add .husky/pre-commit "npm run pre-commit"
๐๏ธ Complex Workflow Example
For enterprise projects with multiple checks:
// ๐ข Enterprise configuration types
interface HuskyConfig {
hooks: {
'pre-commit': string[];
'pre-push': string[];
'commit-msg': string[];
};
lintStaged: {
[pattern: string]: string[];
};
commitlint: {
extends: string[];
rules: Record<string, any>;
};
}
// ๐ Configuration factory
class HuskyConfigFactory {
static createTypeScriptConfig(): HuskyConfig {
return {
hooks: {
'pre-commit': [
'lint-staged',
'npm run test:unit',
'npm run typecheck'
],
'pre-push': [
'npm run test:integration',
'npm run build',
'npm run security-check'
],
'commit-msg': [
'commitlint --edit $1'
]
},
lintStaged: {
'*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
'tsc --noEmit'
],
'*.{json,md,yml}': [
'prettier --write'
]
},
commitlint: {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'test', 'chore', 'perf', 'ci', 'build'
]]
}
}
};
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Hooks Not Running
# โ Wrong - forgetting to install husky
git commit -m "my commit"
# No checks run! ๐ฐ
# โ
Correct - ensure husky is installed
npm run prepare # This runs 'husky install'
git commit -m "feat: add awesome feature โจ"
# ๐ All checks run!
๐คฏ Pitfall 2: Slow Pre-commit Hooks
# โ Slow - checking entire codebase
npx husky add .husky/pre-commit "npm run lint && npm run test"
# โ
Fast - only check changed files
npx husky add .husky/pre-commit "lint-staged"
๐ lint-staged config:
{
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write"]
}
}
๐ซ Pitfall 3: Broken Hooks Stop Development
# ๐ก๏ธ Add escape hatch for emergencies
git commit --no-verify -m "hotfix: critical bug fix"
โ ๏ธ Warning: Only use --no-verify
in genuine emergencies!
๐ ๏ธ Best Practices
- ๐ฏ Keep Hooks Fast: Use lint-staged for speed
- ๐ Document Your Hooks: Explain what each hook does
- ๐ก๏ธ Donโt Block Everything: Allow emergency commits
- ๐จ Make Hooks Helpful: Auto-fix issues when possible
- โจ Test Your Hooks: Ensure they work in different scenarios
- ๐ฅ Team Communication: Explain hooks to new team members
๐จ Example Documentation
# ๐ Our Husky Setup
## Pre-commit Hooks
- โ
TypeScript type checking
- ๐จ ESLint with auto-fix
- ๐ Prettier code formatting
- ๐งช Unit tests for changed files
## Pre-push Hooks
- ๐๏ธ Full build test
- ๐ Security vulnerability scan
- ๐ Bundle size check
## Emergency Override
Use `git commit --no-verify` only for critical hotfixes! ๐จ
๐งช Hands-On Exercise
๐ฏ Challenge: Set Up a Complete Husky Workflow
Create a robust development workflow for a TypeScript project:
๐ Requirements:
- โ Pre-commit: Lint, format, and type-check changed files
- ๐งช Pre-commit: Run relevant tests
- ๐๏ธ Pre-push: Ensure the project builds
- ๐ Commit message: Enforce conventional commits
- ๐ Only process changed files for speed
๐ฎ Bonus Points:
- Add a commit message emoji guide
- Create a security check hook
- Set up automatic dependency updates validation
- Add a hook to check bundle size
๐ก Solution
๐ Click to see solution
# ๐ Complete setup script!
# 1. Install dependencies
npm install --save-dev husky lint-staged @commitlint/cli @commitlint/config-conventional
# 2. Initialize husky
npx husky install
npm pkg set scripts.prepare="husky install"
# 3. Create pre-commit hook
npx husky add .husky/pre-commit "lint-staged"
# 4. Create pre-push hook
npx husky add .husky/pre-push "npm run build && npm run test:integration"
# 5. Create commit message hook
npx husky add .husky/commit-msg "npx --no -- commitlint --edit \$1"
package.json configuration:
{
"scripts": {
"prepare": "husky install",
"typecheck": "tsc --noEmit",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"format": "prettier --write src/**/*.{ts,tsx,json,md}",
"test": "jest",
"test:integration": "jest --config=jest.integration.config.js",
"build": "tsc && vite build",
"bundle-size": "bundlesize"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"tsc --noEmit"
],
"*.{json,md}": [
"prettier --write"
]
},
"commitlint": {
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [2, "always", [
"feat", "fix", "docs", "style", "refactor",
"test", "chore", "perf", "ci", "build"
]],
"subject-case": [2, "always", "sentence-case"]
}
},
"bundlesize": [
{
"path": "./dist/bundle.js",
"maxSize": "100kb"
}
]
}
๐ Security check script (.husky/pre-push):
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "๐๏ธ Building project..."
npm run build
echo "๐งช Running integration tests..."
npm run test:integration
echo "๐ Checking for security vulnerabilities..."
npm audit --audit-level high
echo "๐ฆ Checking bundle size..."
npm run bundle-size
echo "โ
All pre-push checks passed! ๐"
๐ Commit message guide (docs/COMMIT_GUIDE.md):
# ๐ Commit Message Guide
Use this format: `type(scope): description`
## Types
- feat โจ: New feature
- fix ๐: Bug fix
- docs ๐: Documentation
- style ๐จ: Code style changes
- refactor โป๏ธ: Code refactoring
- test ๐งช: Adding tests
- chore ๐ง: Maintenance tasks
## Examples
- `feat(auth): add user login ๐`
- `fix(cart): resolve checkout bug ๐`
- `docs(readme): update installation guide ๐`
๐ Key Takeaways
Youโve learned so much about Husky and Git hooks! Hereโs what you can now do:
- โ Set up Husky in any TypeScript project ๐ช
- โ Create pre-commit workflows that catch issues early ๐ก๏ธ
- โ Optimize hook performance with lint-staged ๐
- โ Enforce code quality across your entire team ๐ฏ
- โ Debug common hook issues like a pro ๐
- โ Build bulletproof development workflows โ๏ธ
Remember: Husky is your development assistant, not your enemy! Itโs here to help you ship better code faster. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Git hooks with Husky!
Hereโs what to do next:
- ๐ป Set up Husky in your current TypeScript project
- ๐๏ธ Create a comprehensive pre-commit workflow
- ๐ Move on to our next tutorial: ESLint with TypeScript
- ๐ Share your Husky setup with your team!
Remember: Every smooth deployment started with good development practices. Keep automating, keep improving, and most importantly, keep coding! ๐
Happy coding! ๐๐โจ