Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to the world of TypeScript ESLint rules! ๐ In this comprehensive guide, weโll explore how to set up and configure ESLint to work seamlessly with TypeScript, ensuring your code is not only type-safe but also follows consistent styling and best practices.
Youโll discover how proper linting can transform your development experience from chaotic to organized ๐งน. Whether youโre building web applications ๐, server-side APIs ๐ฅ๏ธ, or libraries ๐, understanding ESLint with TypeScript is essential for writing robust, maintainable code that your team will love.
By the end of this tutorial, youโll feel confident setting up comprehensive linting rules that catch errors before they reach production! Letโs dive in! ๐โโ๏ธ
๐ Understanding TypeScript ESLint
๐ค What is TypeScript ESLint?
TypeScript ESLint is like having a super-smart code reviewer ๐จโ๐ป that never sleeps! Think of it as your coding mentor that whispers helpful suggestions while you type, catching potential issues before they become bugs.
In TypeScript terms, ESLint provides static analysis of your code to identify problematic patterns ๐. This means you can:
- โจ Catch errors before runtime
- ๐ Enforce consistent code style across your team
- ๐ก๏ธ Prevent common TypeScript pitfalls
- ๐ Improve code readability and maintainability
๐ก Why Use ESLint with TypeScript?
Hereโs why developers love TypeScript ESLint:
- Type-Aware Linting ๐: Goes beyond syntax to understand your types
- Better Team Collaboration ๐ป: Consistent code style across developers
- Preventive Medicine ๐: Catches issues before they become problems
- IDE Integration ๐ง: Real-time feedback while coding
Real-world example: Imagine building an e-commerce platform ๐. With TypeScript ESLint, you can catch issues like unused variables, inconsistent naming, and potential null pointer exceptions before your customers encounter them!
๐ง Basic Setup and Configuration
๐ Installation and Initial Setup
Letโs start with a practical example of setting up ESLint with TypeScript:
# ๐ Install the essential packages
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
# ๐จ For additional style rules
npm install --save-dev eslint-config-prettier eslint-plugin-prettier
# โก Quick setup with initialization
npx eslint --init
๐ก Explanation: Weโre installing the core ESLint packages plus TypeScript-specific plugins. The @typescript-eslint/parser
helps ESLint understand TypeScript syntax, while the plugin provides TypeScript-specific rules.
๐ฏ Basic ESLint Configuration
Hereโs a starter configuration that works great:
// ๐๏ธ .eslintrc.json - Your linting blueprint
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
// ๐จ Custom rules we'll explore
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "error"
}
}
๐ก Practical Examples
๐ Example 1: E-commerce Product Catalog
Letโs build a type-safe product system with proper linting:
// ๐๏ธ Product interface with ESLint-compliant structure
interface Product {
id: string;
name: string;
price: number;
category: ProductCategory;
emoji: string; // Every product needs personality! โจ
inStock: boolean;
}
// ๐ท๏ธ Enum for categories (ESLint loves this!)
enum ProductCategory {
ELECTRONICS = 'electronics',
CLOTHING = 'clothing',
BOOKS = 'books',
FOOD = 'food'
}
// ๐ Product manager class following ESLint best practices
class ProductCatalog {
private readonly products: Map<string, Product> = new Map();
// โ Add product with proper type annotations
addProduct(product: Product): void {
// ๐ก๏ธ ESLint will catch if we forget validation
if (!product.id || !product.name) {
throw new Error('Product must have id and name! ๐ซ');
}
this.products.set(product.id, product);
console.log(`Added ${product.emoji} ${product.name} to catalog!`);
}
// ๐ Search products with type safety
searchByCategory(category: ProductCategory): Product[] {
const results: Product[] = [];
for (const product of this.products.values()) {
if (product.category === category) {
results.push(product);
}
}
return results;
}
// ๐ฐ Calculate total inventory value
calculateTotalValue(): number {
let total = 0;
// ๐ ESLint ensures we handle the iteration properly
for (const product of this.products.values()) {
if (product.inStock) {
total += product.price;
}
}
return total;
}
}
// ๐ฎ Let's use it!
const catalog = new ProductCatalog();
catalog.addProduct({
id: '1',
name: 'TypeScript Handbook',
price: 29.99,
category: ProductCategory.BOOKS,
emoji: '๐',
inStock: true
});
๐ฏ ESLint Benefits Here:
- Enforces consistent naming conventions
- Ensures all code paths return values
- Catches potential null reference errors
- Validates proper type annotations
๐ฎ Example 2: Game Score System with Advanced Rules
Letโs implement a gaming system that showcases more ESLint features:
// ๐ Interface with strict typing for game scores
interface GamePlayer {
readonly id: string;
name: string;
level: number;
experience: number;
achievements: readonly string[]; // ๐ก๏ธ Immutable achievements
}
// ๐ฏ Type-safe score calculation utility
type ScoreMultiplier = 1 | 1.5 | 2 | 2.5; // ๐ Only valid multipliers
class GameScoreManager {
private readonly players: Map<string, GamePlayer> = new Map();
private readonly baseScorePerLevel = 100;
// ๐ Create new player with validation
createPlayer(name: string): GamePlayer {
// ๐ซ ESLint catches empty string bugs
if (!name.trim()) {
throw new Error('Player name cannot be empty! ๐
');
}
const player: GamePlayer = {
id: this.generatePlayerId(),
name: name.trim(),
level: 1,
experience: 0,
achievements: ['๐ Welcome to the Game!']
};
this.players.set(player.id, player);
return player;
}
// ๐ Add experience with proper error handling
addExperience(playerId: string, points: number): void {
const player = this.players.get(playerId);
// ๐ก๏ธ ESLint ensures we handle undefined cases
if (!player) {
throw new Error(`Player ${playerId} not found! ๐`);
}
if (points < 0) {
throw new Error('Experience points cannot be negative! โ ๏ธ');
}
// ๐ Update experience and check for level up
const updatedPlayer = { ...player, experience: player.experience + points };
this.checkLevelUp(updatedPlayer);
}
// ๐ Private method for level calculations
private checkLevelUp(player: GamePlayer): void {
const experienceNeeded = this.baseScorePerLevel * player.level;
if (player.experience >= experienceNeeded) {
const newPlayer: GamePlayer = {
...player,
level: player.level + 1,
achievements: [...player.achievements, `๐ Level ${player.level + 1} Achieved!`]
};
this.players.set(player.id, newPlayer);
console.log(`๐ ${player.name} leveled up to ${newPlayer.level}!`);
}
}
// ๐ Calculate score with multiplier
calculateScore(playerId: string, multiplier: ScoreMultiplier = 1): number {
const player = this.players.get(playerId);
if (!player) {
return 0; // ๐ก๏ธ Safe fallback
}
return (player.level * this.baseScorePerLevel + player.experience) * multiplier;
}
// ๐ Private utility method
private generatePlayerId(): string {
return `player_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
}
๐ Advanced ESLint Rules and Configuration
๐งโโ๏ธ Advanced Rule Configuration
When youโre ready to level up, try these advanced ESLint rules:
// ๐ฏ Advanced .eslintrc.json configuration
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "import", "jsdoc"],
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/typescript"
],
"rules": {
// ๐ก๏ธ Type safety rules
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/strict-boolean-expressions": "error",
// ๐จ Code style rules
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
},
{
"selector": "typeAlias",
"format": ["PascalCase"]
},
{
"selector": "variable",
"format": ["camelCase", "UPPER_CASE"]
}
],
// ๐ Import organization
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal"],
"newlines-between": "always"
}
],
// ๐ Documentation requirements
"jsdoc/require-jsdoc": [
"warn",
{
"require": {
"FunctionDeclaration": true,
"ClassDeclaration": true
}
}
]
}
}
๐๏ธ Project-Specific Rule Overrides
For different types of projects, customize your rules:
// ๐ฎ Game development specific overrides
{
"overrides": [
{
"files": ["src/game/**/*.ts"],
"rules": {
"@typescript-eslint/no-magic-numbers": "error",
"@typescript-eslint/prefer-readonly": "error"
}
},
{
"files": ["src/api/**/*.ts"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-floating-promises": "error"
}
},
{
"files": ["**/*.test.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
}
]
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: The โanyโ Escape Hatch
// โ Wrong way - defeating TypeScript's purpose!
function processApiResponse(data: any): any {
return data.someProperty.nestedValue; // ๐ฅ Runtime error waiting to happen!
}
// โ
Correct way - embrace the types!
interface ApiResponse {
data: {
someProperty: {
nestedValue: string;
};
};
status: 'success' | 'error';
}
function processApiResponse(response: ApiResponse): string {
// ๐ก๏ธ TypeScript and ESLint ensure this is safe
if (response.status === 'error') {
throw new Error('API request failed! ๐ซ');
}
return response.data.someProperty.nestedValue;
}
๐คฏ Pitfall 2: Ignoring Promise Handling
// โ Dangerous - floating promises!
async function fetchUserData(userId: string): Promise<void> {
fetchFromDatabase(userId); // ๐ฅ ESLint will catch this!
// Promise is not awaited or handled
}
// โ
Safe - proper async handling!
async function fetchUserData(userId: string): Promise<UserData | null> {
try {
const userData = await fetchFromDatabase(userId);
console.log('โ
User data fetched successfully!');
return userData;
} catch (error) {
console.error('โ ๏ธ Failed to fetch user data:', error);
return null;
}
}
๐ง Pitfall 3: Inconsistent Naming Conventions
// โ Inconsistent naming - ESLint will flag this
interface user_profile { // Should be PascalCase
First_Name: string; // Should be camelCase
LAST_NAME: string; // Should be camelCase
}
// โ
Consistent naming - ESLint approved!
interface UserProfile {
firstName: string;
lastName: string;
email: string;
preferences: UserPreferences;
}
๐ ๏ธ Best Practices
- ๐ฏ Start Strict, Then Relax: Begin with strict rules and adjust based on your teamโs needs
- ๐ Document Rule Choices: Add comments explaining why specific rules are disabled
- ๐ก๏ธ Use Type-Aware Rules: Enable rules that understand your TypeScript types
- ๐จ Automate Formatting: Combine ESLint with Prettier for consistent formatting
- โจ Regular Rule Reviews: Periodically review and update your ESLint configuration
- ๐ Team Consensus: Ensure all team members agree on the linting rules
- ๐ Measure Impact: Track how linting rules affect your bug reports
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Todo API with ESLint
Create a comprehensive todo management system with proper ESLint configuration:
๐ Requirements:
- โ Todo items with priority levels and categories
- ๐ท๏ธ User assignment and due date tracking
- ๐ค User authentication and authorization
- ๐ Notification system for overdue items
- ๐จ Full ESLint compliance with zero warnings
- ๐ Proper error handling throughout
๐ Bonus Points:
- Add custom ESLint rules for your project
- Implement pre-commit hooks with ESLint
- Create ESLint configuration documentation
- Set up automatic fixes for style issues
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe todo API with ESLint best practices!
// ๐ Core interfaces
interface ITodoItem {
readonly id: string;
title: string;
description?: string;
completed: boolean;
priority: TodoPriority;
category: TodoCategory;
assigneeId?: string;
dueDate?: Date;
createdAt: Date;
updatedAt: Date;
}
// ๐ท๏ธ Enums for better type safety
enum TodoPriority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
URGENT = 'urgent'
}
enum TodoCategory {
WORK = 'work',
PERSONAL = 'personal',
SHOPPING = 'shopping',
HEALTH = 'health'
}
// ๐ค User interface
interface IUser {
readonly id: string;
name: string;
email: string;
role: UserRole;
}
enum UserRole {
USER = 'user',
ADMIN = 'admin'
}
/**
* ๐๏ธ TodoManager class - Manages all todo operations
* Follows ESLint best practices for TypeScript
*/
class TodoManager {
private readonly todos: Map<string, ITodoItem> = new Map();
private readonly users: Map<string, IUser> = new Map();
/**
* โ Create a new todo item
* @param todoData - The todo item data
* @param userId - ID of the user creating the todo
* @returns The created todo item
*/
createTodo(todoData: Omit<ITodoItem, 'id' | 'createdAt' | 'updatedAt'>, userId: string): ITodoItem {
// ๐ก๏ธ Validation with proper error handling
if (!todoData.title.trim()) {
throw new Error('Todo title cannot be empty! ๐');
}
if (!this.users.has(userId)) {
throw new Error('User not found! ๐ค');
}
const now = new Date();
const newTodo: ITodoItem = {
...todoData,
id: this.generateTodoId(),
createdAt: now,
updatedAt: now
};
this.todos.set(newTodo.id, newTodo);
console.log(`โ
Created todo: ${newTodo.title}`);
return newTodo;
}
/**
* ๐ Get todos by category
* @param category - The category to filter by
* @returns Array of todos in the specified category
*/
getTodosByCategory(category: TodoCategory): readonly ITodoItem[] {
const results: ITodoItem[] = [];
for (const todo of this.todos.values()) {
if (todo.category === category) {
results.push(todo);
}
}
return Object.freeze(results); // ๐ Return immutable array
}
/**
* โ ๏ธ Get overdue todos
* @returns Array of overdue todos
*/
getOverdueTodos(): readonly ITodoItem[] {
const now = new Date();
const overdueTodos: ITodoItem[] = [];
for (const todo of this.todos.values()) {
if (todo.dueDate && todo.dueDate < now && !todo.completed) {
overdueTodos.push(todo);
}
}
return Object.freeze(overdueTodos);
}
/**
* ๐ Get completion statistics
* @returns Statistics object
*/
getStatistics(): {
total: number;
completed: number;
pending: number;
overdue: number;
completionRate: number;
} {
const total = this.todos.size;
const completed = Array.from(this.todos.values()).filter(todo => todo.completed).length;
const pending = total - completed;
const overdue = this.getOverdueTodos().length;
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
return {
total,
completed,
pending,
overdue,
completionRate
};
}
/**
* ๐ Private utility method to generate unique IDs
* @returns A unique todo ID
*/
private generateTodoId(): string {
return `todo_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
/**
* ๐ค Register a new user
* @param userData - User registration data
* @returns The created user
*/
registerUser(userData: Omit<IUser, 'id'>): IUser {
if (!userData.email.includes('@')) {
throw new Error('Invalid email address! ๐ง');
}
const newUser: IUser = {
...userData,
id: `user_${Date.now()}`
};
this.users.set(newUser.id, newUser);
console.log(`๐ค Registered user: ${newUser.name}`);
return newUser;
}
}
// ๐ฎ Example usage with proper error handling
try {
const todoManager = new TodoManager();
// Register a user
const user = todoManager.registerUser({
name: 'Sarah Developer',
email: '[email protected]',
role: UserRole.USER
});
// Create some todos
const workTodo = todoManager.createTodo({
title: 'Review TypeScript ESLint rules',
description: 'Study the latest ESLint configuration options',
completed: false,
priority: TodoPriority.HIGH,
category: TodoCategory.WORK,
dueDate: new Date('2024-01-15')
}, user.id);
// Get statistics
const stats = todoManager.getStatistics();
console.log('๐ Todo Statistics:', stats);
} catch (error) {
console.error('โ Error:', error);
}
๐ฏ ESLint Configuration for this project:
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "jsdoc"],
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/prefer-readonly": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
}
],
"jsdoc/require-jsdoc": [
"error",
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true
}
}
]
}
}
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Set up ESLint with TypeScript configuration like a pro ๐ช
- โ Configure advanced rules that catch bugs before they happen ๐ก๏ธ
- โ Apply best practices for team collaboration ๐ฏ
- โ Debug linting issues efficiently ๐
- โ Build type-safe applications with proper code quality! ๐
Remember: ESLint is your coding buddy, not your enemy! Itโs here to help you write better, more maintainable code. The initial setup might feel strict, but your future self will thank you! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered TypeScript ESLint rules and best practices!
Hereโs what to do next:
- ๐ป Set up ESLint in your current TypeScript project
- ๐๏ธ Experiment with different rule configurations for your team
- ๐ Move on to our next tutorial: [Prettier Integration with TypeScript]
- ๐ Share your ESLint configurations with the community!
Remember: Every TypeScript expert was once figuring out their first ESLint rule. Keep coding, keep learning, and most importantly, keep your code clean! ๐
Happy coding! ๐๐โจ