3 min read
On this page

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 any to silence TypeScript: every any is a hole in your type safety. Use unknown and 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-prettier as the last config to disable ESLint rules that Prettier handles.
  • Skipping type checking in CI: run tsc --noEmit in 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: true in TypeScript, eslint-config-prettier to avoid conflicts, and format-on-save in your editor
  • Run tsc --noEmit, eslint ., and prettier --check . in CI to catch issues before merge