TypeScript & Linting
TypeScript adds static types to JavaScript. ESLint catches code quality issues. Prettier formats your code consistently. Together, they eliminate entire categories of bugs and stop style debates before they start.
TypeScript: Static Types for JavaScript
TypeScript is a superset of JavaScript that adds optional type annotations. It compiles to plain JavaScript. The type system catches errors at compile time that would otherwise be runtime bugs.
Basic Types
// Primitive types
const name: string = 'Ada';
const age: number = 36;
const active: boolean = true;
// Arrays
const scores: number[] = [95, 87, 92];
const names: Array<string> = ['Ada', 'Grace'];
// Object type
const user: { name: string; age: number } = {
name: 'Ada',
age: 36,
};
// Union types
let id: string | number = 'abc';
id = 123; // Also valid
// Optional properties
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
Interfaces
Interfaces describe the shape of objects. They are the primary way to define contracts in TypeScript.
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
createdAt: Date;
}
function createUser(data: Omit<User, 'id' | 'createdAt'>): User {
return {
...data,
id: generateId(),
createdAt: new Date(),
};
}
// TypeScript catches mistakes immediately
createUser({
name: 'Ada',
email: 'ada@example.com',
role: 'superadmin', // Error: Type '"superadmin"' is not assignable to type '"admin" | "editor" | "viewer"'
});
Generics
Generics let you write reusable code that works with different types.
// A function that works with any type
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // Type: number | undefined
const str = firstElement(['a', 'b']); // Type: string | undefined
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface Product {
id: number;
name: string;
price: number;
}
async function fetchProducts(): Promise<ApiResponse<Product[]>> {
const response = await fetch('/api/products');
return response.json();
}
Utility Types
TypeScript includes built-in types for common transformations.
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Pick specific properties
type PublicUser = Pick<User, 'id' | 'name'>;
// Omit specific properties
type CreateUserInput = Omit<User, 'id'>;
// Make all properties optional
type UserUpdate = Partial<User>;
// Make all properties required
type StrictUser = Required<User>;
// Make all properties readonly
type FrozenUser = Readonly<User>;
// Record type for dictionaries
type UserRoles = Record<string, 'admin' | 'editor' | 'viewer'>;
Why TypeScript Catches Bugs Tests Do Not
TypeScript catches structural errors before any code runs.
// Bug: property name typo
// JavaScript: silently returns undefined
// TypeScript: compile error
interface Config {
apiUrl: string;
timeout: number;
}
function initApp(config: Config) {
// TypeScript catches: Property 'apiurl' does not exist on type 'Config'
// Did you mean 'apiUrl'?
fetch(config.apiurl);
}
// Bug: forgot to handle a case
type Status = 'pending' | 'active' | 'cancelled' | 'expired';
function getStatusColor(status: Status): string {
switch (status) {
case 'pending': return 'yellow';
case 'active': return 'green';
case 'cancelled': return 'red';
// TypeScript error: Not all code paths return a value
// You forgot 'expired'
}
}
Tests verify behavior. Types verify structure. You need both, but types cover the boring structural bugs automatically and exhaustively.
ESLint: Consistent Code & Catching Mistakes
ESLint analyzes your code for patterns that are likely bugs, poor practices, or style inconsistencies.
Modern ESLint Setup (Flat Config)
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': 'error',
'no-console': 'warn',
},
},
{
ignores: ['dist/', 'node_modules/'],
},
);
What ESLint Catches
// Unused variables
const unused = 'never referenced'; // ESLint: 'unused' is defined but never used
// Accidental assignment in condition
if (x = 5) { } // ESLint: Expected a conditional expression, saw an assignment
// Unreachable code
function calc() {
return 42;
console.log('never runs'); // ESLint: Unreachable code
}
// Prefer const
let name = 'Ada'; // ESLint: 'name' is never reassigned, use 'const'
Running ESLint
npx eslint . # Lint all files
npx eslint . --fix # Auto-fix what can be fixed
npx eslint src/main.ts # Lint a specific file
Prettier: Opinionated Formatting
Prettier formats your code automatically. It makes decisions about spacing, line breaks, quotes, and semicolons so your team does not have to debate them.
Configuration
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
Save this as .prettierrc in your project root.
Running Prettier
npx prettier . --write # Format all files in place
npx prettier . --check # Check if files are formatted (CI)
npx prettier src/main.ts --write # Format a specific file
The Setup: TypeScript + ESLint + Prettier
Step 1: Initialize the Project
npm init -y
npm install -D typescript @types/node
npx tsc --init
Step 2: Add ESLint
npm install -D eslint @eslint/js typescript-eslint
Create eslint.config.js as shown above.
Step 3: Add Prettier
npm install -D prettier eslint-config-prettier
Add eslint-config-prettier to your ESLint config as the last entry to disable conflicting rules.
Step 4: Add Scripts
{
"scripts": {
"check": "tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier . --write",
"format:check": "prettier . --check"
}
}
Common Pitfalls
- Using
anyto silence TypeScript: everyanyis a hole in your type safety. Useunknownand narrow the type instead. - Fighting Prettier: Prettier is opinionated by design. The value is that everyone stops debating formatting. Accept the defaults and move on.
- Not using
strict: true: TypeScript's strict mode enables the most valuable checks (strictNullChecks,noImplicitAny, etc.). Non-strict TypeScript misses many bugs. - ESLint and Prettier conflicts: always include
eslint-config-prettieras the last config to disable ESLint rules that Prettier handles. - Skipping type checking in CI: run
tsc --noEmitin your CI pipeline. It catches type errors that editors might miss. - Over-typing: not everything needs an explicit type. TypeScript infers types well. Use explicit types for function parameters and return types, not for every local variable.
Key Takeaways
- TypeScript adds static types that catch structural bugs JavaScript tests miss
- Interfaces define object shapes, generics enable reusable typed code, utility types transform existing types
- ESLint catches code quality issues and enforces consistent patterns
- Prettier formats code automatically so teams stop debating style
- The three tools work together: TypeScript for types, ESLint for logic and patterns, Prettier for formatting
- Use
strict: truein TypeScript,eslint-config-prettierto avoid conflicts, and format-on-save in your editor - Run
tsc --noEmit,eslint ., andprettier --check .in CI to catch issues before merge