Node.js with TypeScript: Complete Theory & Practice Guide

Build safer Node.js apps with static typing, strong tooling, and scalable patterns.

Type SafetyGenericsDecorators

Table of Contents

  1. Theory: Type System Fundamentals
  2. Theory: Compilation & Runtime
  3. Basic: Setup & Configuration
  4. Theory: Type Safety Benefits
  5. Advanced: Design Patterns
  6. Advanced: Decorators & Metadata
  7. Theory: Integration Strategies
  8. Best Practices
  9. Interview Q&A + MCQ
  10. Contextual Learning Links

1. Theory: Type System Fundamentals

TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It adds optional types, classes, interfaces, and modern ECMAScript features.

┌─────────────────────────────────────────────────────────────┐
│                    TypeScript Compilation                    │
├─────────────────────────────────────────────────────────────┤
│   TypeScript Code (.ts)      JavaScript Code (.js)          │
│   function greet(name: string): string { ... }              │
│             tsc compiles and erases types                   │
│   function greet(name) { ... }                              │
│   • Types removed at compile time                           │
│   • Zero runtime overhead                                   │
│   • 100% JavaScript compatibility                           │
└─────────────────────────────────────────────────────────────┘
AspectJavaScript (Dynamic)TypeScript (Static)
Type checkingRuntimeCompile-time
Error detectionWhen code runsBefore code runs
IDE supportLimitedExcellent
DocumentationComments neededTypes as docs
Refactoring safetyRiskySafe
type TypeHierarchy = {
    'any': 'Opt-out of type checking (avoid)',
    'unknown': 'Type-safe alternative to any',
    'void': 'No return value',
    'never': 'Function never returns',
    'Primitives': 'string, number, boolean, symbol, bigint',
    'Literal types': '"red" | "blue"',
    'Union types': 'string | number',
    'Intersection types': 'A & B',
    'Generics': 'Type variables (T, U, V)'
};
const typescriptBenefits = {
    earlyErrorDetection: {
        problem: 'Cannot read property on undefined',
        solution: 'Caught at compile time'
    },
    ideSupport: ['Autocomplete', 'Rename refactor', 'Find references'],
    selfDocumenting: 'Types communicate API contracts',
    refactoring: 'Type changes reveal impacted code instantly'
};

2. Theory: Compilation & Runtime

┌─────────────────────────────────────────────────────────────────┐
│                    TypeScript Compilation Flow                   │
├─────────────────────────────────────────────────────────────────┤
│ .ts/.tsx → Parser → AST → Type Checker → Transformer → .js/.d.ts │
│          syntax     type errors     target config output        │
└─────────────────────────────────────────────────────────────────┘
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noImplicitReturns": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "declaration": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
// TypeScript source
interface User { id: number; name: string; email: string; }
class UserService {
  async getUser(id: number): Promise { return db.findUser(id); }
}

// Compiled JS (types erased)
class UserService {
  async getUser(id) { return db.findUser(id); }
}

3. Basic: Setup & Configuration

# Initialize project
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init
npm install --save-dev nodemon ts-node
{
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "start": "node dist/server.js",
    "dev": "nodemon --exec ts-node src/server.ts",
    "type-check": "tsc --noEmit",
    "clean": "rm -rf dist"
  }
}
// src/server.ts
import express, { Request, Response, NextFunction, Application } from 'express';

interface User { id: number; name: string; email: string; createdAt: Date; }
interface CreateUserRequest { name: string; email: string; }
interface ApiResponse { success: boolean; data?: T; error?: string; timestamp: string; }

class UserController {
  private users: User[] = [];
  private nextId = 1;
  async createUser(req: Request<{}, {}, CreateUserRequest>, res: Response>): Promise {
    const { name, email } = req.body;
    if (!name || !email) { res.status(400).json({ success: false, error: 'Name and email are required', timestamp: new Date().toISOString() }); return; }
    const newUser: User = { id: this.nextId++, name, email, createdAt: new Date() };
    this.users.push(newUser);
    res.status(201).json({ success: true, data: newUser, timestamp: new Date().toISOString() });
  }
}

const app: Application = express();
app.use(express.json());
const userController = new UserController();
app.post('/api/users', (req, res) => userController.createUser(req, res));
const PORT: number = parseInt(process.env.PORT || '3000', 10);
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

4. Theory: Type Safety Benefits

type Status = 'pending' | 'approved' | 'rejected';
type ID = string | number;

function processInput(input: string | number): string {
  if (typeof input === 'string') return input.toUpperCase();
  return input.toFixed(2);
}

function isUser(obj: any): obj is User {
  return obj && typeof obj.id === 'number' && typeof obj.name === 'string';
}
type ApiState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };
interface Repository {
  findById(id: ID): Promise;
  findAll(): Promise;
  create(entity: Omit): Promise;
  update(id: ID, entity: Partial): Promise;
  delete(id: ID): Promise;
}
// Utility types
type ProductUpdate = Partial;
type CompleteProduct = Required;
type ImmutableProduct = Readonly;
type ProductPreview = Pick;
type ProductWithoutDates = Omit;
type UserRoles = Record;

5. Advanced: Design Patterns

// Dependency Injection sketch
interface Database { query(sql: string, params?: any[]): Promise; }
interface Logger { info(message: string, metadata?: Record): void; }
interface UserRepository { findById(id: number): Promise; }

class UserService {
  constructor(private readonly userRepository: UserRepository, private readonly logger: Logger) {}
  async getUser(id: number): Promise {
    const user = await this.userRepository.findById(id);
    if (!user) throw new Error('User not found');
    return user;
  }
}
// Result type pattern
type Result =
  | { success: true; value: T }
  | { success: false; error: E };

class ResultFactory {
  static ok(value: T): Result { return { success: true, value }; }
  static err(error: E): Result { return { success: false, error }; }
}

6. Advanced: Decorators & Metadata

// Enable: "experimentalDecorators": true, "emitDecoratorMetadata": true
function Get(path: string = '/'): MethodDecorator {
  return (target: any, propertyKey: string | symbol) => {
    const routes = Reflect.getMetadata('routes', target.constructor) || [];
    routes.push({ method: 'get', path, handler: propertyKey });
    Reflect.defineMetadata('routes', routes, target.constructor);
  };
}

function Controller(basePath: string): ClassDecorator {
  return (target: any) => Reflect.defineMetadata('basePath', basePath, target);
}
@Controller('/api/users')
class UserController {
  @Get('/:id')
  async getUser(@Param('id') id: string): Promise {
    return await this.userService.getUser(parseInt(id));
  }
}

7. Theory: Integration Strategies

const migrationStrategies = {
  tsconfig: {
    compilerOptions: { allowJs: true, checkJs: true, outDir: './dist' },
    include: ['src/**/*.ts', 'src/**/*.js']
  },
  incremental: {
    step1: 'Rename .js to .ts',
    step2: 'Fix obvious type errors (use any temporarily)',
    step3: 'Add explicit types and remove any'
  }
};
// Extending Express types
declare global {
  namespace Express {
    interface Request {
      user?: { id: number; role: 'admin' | 'user' };
      startTime: number;
    }
    interface Response {
      sendJson(data: T): this;
    }
  }
}

8. Best Practices

const typescriptStandards = {
  explicitTypes: 'Use explicit function parameter/return types',
  avoidImplicitAny: 'Enable noImplicitAny and avoid fallback any',
  constAssertions: 'Use as const for precise literal types',
  discriminatedUnions: 'Model state machines safely',
  readonlyData: 'Mark immutable fields and inputs readonly'
};
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": true,
    "incremental": true
  }
}
// Common TS error examples
const user = await db.findUser(id);
console.log(user.name);   // ❌ possibly undefined
console.log(user?.name);  // ✅ safe

const value: unknown = getValue();
if (typeof value === 'string') console.log(value.toUpperCase()); // ✅
DecisionRecommendationRationale
strict modeAlways enableCatches bug classes early
target versionES2022+Modern runtime capabilities
declaration filesYes for librariesBetter consumer IDE support
source mapsEnableDebug TS via original source
incremental buildsEnableFaster large-project recompiles
# Quick commands
npm install -D typescript @types/node
npx tsc --init
npx tsc
npx tsc --watch
npx tsc --noEmit
npx ts-node src/server.ts
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

10 Interview Questions + 10 MCQs

1What is TypeScript in one line?easy
Answer: A statically typed superset of JavaScript that compiles to JavaScript.
2Does TypeScript add runtime overhead for types?easy
Answer: No, type annotations are erased at compile time.
3Why enable strict mode?medium
Answer: It catches null/undefined and implicit typing bugs before runtime.
4What is a discriminated union?medium
Answer: A union of object types identified by a shared literal field (e.g., `status`).
5When use generics?easy
Answer: For reusable, type-safe functions/classes without losing type information.
6What does `unknown` solve versus `any`?medium
Answer: `unknown` forces type narrowing before use, preserving safety.
7Why declaration files (`.d.ts`) matter?hard
Answer: They expose type contracts for library consumers and improve tooling.
8How can TS help refactoring?easy
Answer: Type errors reveal every affected usage instantly after API/type changes.
9When should `allowJs` be used?medium
Answer: During incremental migration from JavaScript codebases.
10What is `tsc --noEmit` used for?easy
Answer: Type-check only, without generating output files.

10 TypeScript MCQs

1

TypeScript compiles to:

AJavaScript
BPython
CBytecode only
DMachine code directly
Explanation: `tsc` transpiles TS to JS.
2

Type checks happen mainly at:

ACompile time
BRuntime only
CDatabase time
DDeploy time only
Explanation: TS static analysis runs before execution.
3

Safer alternative to `any`:

A`unknown`
B`void`
C`never`
D`object`
Explanation: `unknown` requires narrowing before use.
4

`Partial` does what?

AMakes all properties optional
BMakes all readonly
CRemoves null only
DConverts to array
Explanation: `Partial` is useful for update DTOs.
5

`tsc --noEmit` is used for:

AType-checking without output
BRunning tests
CBundling CSS
DLinting JSON
Explanation: Great for CI type-check stages.
6

Discriminated unions are best for:

AModeling state machines
BCompressing files
CSQL migrations
DNode version switching
Explanation: A shared tag field enables safe narrowing.
7

`Pick`:

ASelects specific keys from a type
BDeletes all keys
CConverts to union automatically
DDisables strict mode
Explanation: Useful for view models and DTO subsets.
8

`strictNullChecks` primarily prevents:

AUnsafe null/undefined access
BES module imports
CDocker builds
DHTTP requests
Explanation: It enforces handling missing values safely.
9

Generics provide:

AReusable type-safe abstractions
BRuntime type conversion
CAutomatic DB indexing
DNo compile checks
Explanation: Generic APIs preserve type information across use cases.
10

`allowJs` is useful for:

AIncremental JS → TS migration
BDisabling module resolution
CRemoving all tsconfig options
DMaking code run faster automatically
Explanation: It lets mixed JS/TS codebases compile together.