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
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 │
└─────────────────────────────────────────────────────────────┘
| Aspect | JavaScript (Dynamic) | TypeScript (Static) |
|---|---|---|
| Type checking | Runtime | Compile-time |
| Error detection | When code runs | Before code runs |
| IDE support | Limited | Excellent |
| Documentation | Comments needed | Types as docs |
| Refactoring safety | Risky | Safe |
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()); // ✅
| Decision | Recommendation | Rationale |
|---|---|---|
| strict mode | Always enable | Catches bug classes early |
| target version | ES2022+ | Modern runtime capabilities |
| declaration files | Yes for libraries | Better consumer IDE support |
| source maps | Enable | Debug TS via original source |
| incremental builds | Enable | Faster 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:
Explanation: `tsc` transpiles TS to JS.
2
Type checks happen mainly at:
Explanation: TS static analysis runs before execution.
3
Safer alternative to `any`:
Explanation: `unknown` requires narrowing before use.
4`Partial
`Partial` does what?
Explanation: `Partial` is useful for update DTOs.
5
`tsc --noEmit` is used for:
Explanation: Great for CI type-check stages.
6
Discriminated unions are best for:
Explanation: A shared tag field enables safe narrowing.
7`Pick
`Pick`:
Explanation: Useful for view models and DTO subsets.
8
`strictNullChecks` primarily prevents:
Explanation: It enforces handling missing values safely.
9
Generics provide:
Explanation: Generic APIs preserve type information across use cases.
10
`allowJs` is useful for:
Explanation: It lets mixed JS/TS codebases compile together.