TypeScript Cheat Sheet
TypeScript setup, language fundamentals, and Next.js App Router patterns. Targets TypeScript 5.x and Next.js 14+.
Versions: TypeScript 5.x · Next.js 14+ · Node 22+ · pnpm 9+ · Vitest 1.x · pino 8.x+
Tooling & Setup
nvm - Node Version Manager
# Install nvm (Linux/macOS) - always fetches the latest release
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name"' | cut -d'"' -f4)/install.sh | bash
source ~/.bashrc
# Install and use a Node version
nvm install 22
nvm use 22
nvm alias default 22
# List installed versions
nvm ls
# Use .nvmrc in a project directory
echo "22" > .nvmrc
nvm use # reads .nvmrc automatically
# Install nvm on Windows (nvm-windows) - always fetches the latest release
$nvmVersion = (Invoke-RestMethod https://api.github.com/repos/coreybutler/nvm-windows/releases/latest).tag_name
winget install --id CoreyButler.NVMforWindows --version $nvmVersion
nvm install 22 && nvm use 22Project bootstrap
# New Next.js project (TypeScript by default)
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
# Plain TypeScript project
mkdir my-lib && cd my-lib
npm init -y
npm install -D typescript @types/node ts-node
npx tsc --init
# Run TypeScript without compiling
npx ts-node src/index.ts
npx tsx src/index.ts # faster alternative (no type checking)
# Type-check only (no emit)
npx tsc --noEmit
# Watch mode
npx tsc --watchtsconfig.json - recommended base
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Package management
# pnpm (faster, strict node_modules)
npm install -g pnpm
pnpm install
pnpm add express
pnpm add -D @types/express vitest
# Useful scripts in package.json
# "typecheck": "tsc --noEmit"
# "lint": "eslint src --ext .ts,.tsx"
# "build": "tsc"
# "dev": "tsx watch src/index.ts"Core Types
Primitives and literals
const name: string = 'Alice'
const age: number = 30
const active: boolean = true
const id: bigint = 9007199254740993n
const sym: symbol = Symbol('key')
// Literal types
type Direction = 'north' | 'south' | 'east' | 'west'
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500
// Template literal types
type EventName = `on${Capitalize<string>}`
type CSSUnit = `${number}px` | `${number}rem` | `${number}%`
type ApiPath = `/api/${string}`Arrays and tuples
// Arrays
const names: string[] = ['Alice', 'Bob']
const ids: Array<number> = [1, 2, 3]
const matrix: number[][] = [[1, 2], [3, 4]]
// Readonly array
const frozen: ReadonlyArray<string> = ['x', 'y']
// Tuples
type Point = [number, number]
type Entry = [string, number, boolean?] // optional third element
type Rest = [string, ...number[]] // rest element
const point: Point = [10, 20]
const [x, y] = pointObjects and interfaces
// Interface (extendable, declaration-mergeable)
interface User {
readonly id: string
name: string
email: string
role?: 'admin' | 'user' // optional
}
// Extend
interface AdminUser extends User {
permissions: string[]
}
// Type alias (can express unions, intersections, mapped types)
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string }
// Intersection
type WithTimestamps = { createdAt: Date; updatedAt: Date }
type UserRecord = User & WithTimestamps
// Index signature
interface StringMap {
[key: string]: string
}
// Record shorthand
type Env = Record<string, string | undefined>Unknown, never, any
// unknown - safe alternative to any; must narrow before use
function parse(raw: unknown): User {
if (typeof raw !== 'object' || raw === null) throw new Error('Not an object')
const obj = raw as Record<string, unknown>
if (typeof obj.id !== 'string') throw new Error('Bad id')
return raw as User
}
// never - exhaustive checks
type Shape = { kind: 'circle'; r: number } | { kind: 'rect'; w: number; h: number }
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.r ** 2
case 'rect': return s.w * s.h
default:
const _exhaustive: never = s
throw new Error(`Unhandled: ${_exhaustive}`)
}
}Type Narrowing & Guards
// typeof guard
function double(x: string | number) {
if (typeof x === 'string') return x.repeat(2)
return x * 2
}
// instanceof guard
function formatDate(d: Date | string) {
if (d instanceof Date) return d.toISOString()
return new Date(d).toISOString()
}
// in operator
interface Cat { meow(): void }
interface Dog { bark(): void }
function speak(animal: Cat | Dog) {
if ('meow' in animal) animal.meow()
else animal.bark()
}
// Type predicate (user-defined guard)
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value && typeof (value as User).id === 'string' &&
'name' in value && typeof (value as User).name === 'string'
)
}
// Assertion function (throws if not valid)
function assertString(val: unknown): asserts val is string {
if (typeof val !== 'string') throw new TypeError(`Expected string, got ${typeof val}`)
}Generics
// Basic generic function
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
// Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
// Default type parameter
function createState<T = string>(): { value: T | null; set: (v: T) => void } {
let value: T | null = null
return { value, set: (v) => { value = v } }
}
// Generic interface
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>
findAll(): Promise<T[]>
save(entity: T): Promise<T>
delete(id: ID): Promise<void>
}
// Conditional types
type Flatten<T> = T extends Array<infer Item> ? Item : T
type IsString<T> = T extends string ? true : false
// Infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type Awaited<T> = T extends Promise<infer V> ? V : TUtility Types
interface User {
id: string
name: string
email: string
password: string
role: 'admin' | 'user'
}
type PartialUser = Partial<User> // all fields optional
type RequiredUser = Required<User> // all fields required
type ReadonlyUser = Readonly<User> // all fields readonly
type PublicUser = Omit<User, 'password'> // exclude a key
type LoginFields = Pick<User, 'email' | 'password'> // only these keys
type UserRole = User['role'] // index access type
// NonNullable
type MaybeString = string | null | undefined
type DefiniteStr = NonNullable<MaybeString> // string
// Parameters and ReturnType
function createUser(name: string, role: User['role']): User { /* ... */ }
type CreateArgs = Parameters<typeof createUser> // [string, User['role']]
type CreatedUser = ReturnType<typeof createUser> // User
// Extract / Exclude
type AdminOrUser = Extract<'admin' | 'user' | 'guest', 'admin' | 'user'> // 'admin' | 'user'
type NonAdmin = Exclude<'admin' | 'user' | 'guest', 'admin'> // 'user' | 'guest'
// Mapped type
type Optional<T> = { [K in keyof T]?: T[K] }
type Mutable<T> = { -readonly [K in keyof T]: T[K] }Classes
abstract class BaseRepository<T> {
protected readonly tableName: string
constructor(tableName: string) {
this.tableName = tableName
}
abstract findById(id: string): Promise<T | null>
protected log(msg: string): void {
console.log(`[${this.tableName}] ${msg}`)
}
}
class UserRepository extends BaseRepository<User> {
constructor(private readonly db: DatabaseClient) {
super('users')
}
async findById(id: string): Promise<User | null> {
this.log(`findById ${id}`)
return this.db.query<User>(`SELECT * FROM users WHERE id = $1`, [id])
}
// Static factory
static create(db: DatabaseClient): UserRepository {
return new UserRepository(db)
}
}
// Class implementing an interface
interface Serializable {
serialize(): string
deserialize(s: string): this
}
class Config implements Serializable {
constructor(public readonly data: Record<string, string> = {}) {}
serialize(): string { return JSON.stringify(this.data) }
deserialize(s: string): this {
return new (this.constructor as typeof Config)(JSON.parse(s)) as this
}
}Enums & Discriminated Unions
// Const enum (inlined at compile time - no runtime object)
const enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
// Discriminated union (prefer over enum for extensibility)
type LoadingState = { status: 'idle' }
type LoadState = { status: 'loading' }
type SuccessState<T> = { status: 'success'; data: T }
type ErrorState = { status: 'error'; error: Error }
type AsyncState<T> = LoadingState | LoadState | SuccessState<T> | ErrorState
function render<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle': return 'Waiting...'
case 'loading': return 'Loading...'
case 'success': return `Data: ${JSON.stringify(state.data)}`
case 'error': return `Error: ${state.error.message}`
}
}Async & Error Handling
// Result type - avoids throwing across boundaries
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
async function fetchUser(id: string): Promise<Result<User>> {
try {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) }
return { ok: true, value: await res.json() as User }
} catch (err) {
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) }
}
}
// Usage
const result = await fetchUser('123')
if (result.ok) {
console.log(result.value.name)
} else {
console.error(result.error.message)
}
// Promise combinators
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
])
// Settle all (never rejects)
const results = await Promise.allSettled([fetchUser('1'), fetchUser('2')])
for (const r of results) {
if (r.status === 'fulfilled') console.log(r.value)
else console.error(r.reason)
}
// Typed fetch wrapper
async function apiFetch<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, { ...init, headers: { 'Content-Type': 'application/json', ...init?.headers } })
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`)
return res.json() as Promise<T>
}Logging
🛠️ Deeper reference - this section covers structured JSON logging with pino, child loggers,
AsyncLocalStoragefor request-scoped context, the decorator pattern, and log-level guidance. It’s a mini-guide, not a quick-reference snippet.
console.log is fine for scripts and the dev loop, but in any server-side TypeScript (Next.js Route Handlers, Node services, AWS Lambda, Azure Functions) it’s the wrong tool: no levels, no structured fields, no redaction, and synchronous I/O on the event loop. The de-facto Node logger is pino - fast, JSON-by-default, and integrates cleanly with Next.js, Fastify, NestJS, and OpenTelemetry.
pino - structured JSON logging ✅ Current preferred logger for server-side TypeScript (as of 2026)
pnpm add pino
pnpm add -D pino-pretty # dev-only - human-readable output// src/lib/logger.ts
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: { service: 'my-api', env: process.env.NODE_ENV },
timestamp: pino.stdTimeFunctions.isoTime,
// Redact common secret fields - applied to every log payload
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'*.password',
'*.token',
'*.connectionString',
],
censor: '[REDACTED]',
},
// Pretty-print in dev only - in prod, stay as JSON for log shippers
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:HH:MM:ss.l' } }
: undefined,
})Use it with structured fields, not interpolation - the first arg is a context object, the second is the message:
import { logger } from '@/lib/logger'
logger.info({ orderId, customerId, elapsedMs }, 'order processed')
logger.warn({ vault: vaultName }, 'soft-delete disabled')
logger.error({ err, exitCode: rc }, 'apply failed') // pino auto-serialises Error objectsChild loggers - bind context once, reuse everywhere
// Bind a correlation id / request id once - every line through the child carries it.
const reqLog = logger.child({ correlationId: req.headers['x-correlation-id'], userId: user.id })
reqLog.info('checkout starting')
await runCheckout()
reqLog.info({ orderId }, 'checkout complete')Request-scoped context with AsyncLocalStorage
In an HTTP handler you usually don’t want to thread reqLog through every function call. AsyncLocalStorage (Node 14+) gives you implicit per-request context that survives await boundaries.
// src/lib/request-context.ts
import { AsyncLocalStorage } from 'node:async_hooks'
import { randomUUID } from 'node:crypto'
import { logger } from './logger'
type Context = { correlationId: string; userId?: string }
const als = new AsyncLocalStorage<Context>()
export const requestContext = {
run: <T>(ctx: Context, fn: () => T): T => als.run(ctx, fn),
get: (): Context | undefined => als.getStore(),
}
// A logger that picks up whatever context is current.
export const log = new Proxy(logger, {
get(target, prop: keyof typeof logger) {
const ctx = requestContext.get()
return ctx ? target.child(ctx)[prop] : target[prop]
},
}) as typeof logger// Next.js Route Handler / Express middleware
export async function POST(req: Request) {
return requestContext.run(
{ correlationId: req.headers.get('x-correlation-id') ?? randomUUID() },
async () => {
log.info('handling checkout') // correlationId attached automatically
await processOrder() // nested calls inherit it
return Response.json({ ok: true })
},
)
}Logging decorator pattern
The decorator pattern works the same in TypeScript as in C#: wrap a service in another class that implements the same interface, log around each call, delegate inside.
export interface OrderService {
place(req: PlaceOrderRequest): Promise<Order>
}
export class RealOrderService implements OrderService {
async place(req: PlaceOrderRequest): Promise<Order> { /* real work */ }
}
export class LoggingOrderService implements OrderService {
constructor(private readonly inner: OrderService, private readonly log = logger) {}
async place(req: PlaceOrderRequest): Promise<Order> {
const child = this.log.child({ method: 'place', customerId: req.customerId })
const start = performance.now()
child.info('starting')
try {
const order = await this.inner.place(req)
child.info({ orderId: order.id, elapsedMs: performance.now() - start }, 'ok')
return order
} catch (err) {
child.error({ err, elapsedMs: performance.now() - start }, 'failed')
throw err
}
}
}
// Wire it up
export const orderService: OrderService = new LoggingOrderService(new RealOrderService())For stacked decorators (logging → retry → caching → real), just nest the constructors in the desired order - innermost is the real implementation.
Class method decorator (TypeScript 5.0+ standard decorators)
If you’d rather decorate individual methods than wrap a whole class:
function logged(level: 'info' | 'debug' = 'info') {
return function <This, Args extends unknown[], Return>(
originalMethod: (this: This, ...args: Args) => Promise<Return>,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>,
) {
const name = String(context.name)
return async function (this: This, ...args: Args): Promise<Return> {
const start = performance.now()
logger[level]({ method: name }, 'starting')
try {
const result = await originalMethod.call(this, ...args)
logger[level]({ method: name, elapsedMs: performance.now() - start }, 'ok')
return result
} catch (err) {
logger.error({ method: name, err, elapsedMs: performance.now() - start }, 'failed')
throw err
}
}
}
}
class OrderService {
@logged()
async place(req: PlaceOrderRequest): Promise<Order> { /* ... */ }
}Requires "target": "ES2022" (or later) and not "experimentalDecorators": true in tsconfig.json - the legacy decorator proposal is incompatible with the standard one.
Log levels - when to use which
| Level | Use for |
|---|---|
trace | Per-iteration detail, payload dumps - dev only |
debug | Local diagnostics - off in prod |
info | Business events: request handled, job completed |
warn | Recoverable issue, retry succeeded, fallback used |
error | An operation failed; caller will see a failure |
fatal | Process is unhealthy / about to exit - alert on it |
Sensible defaults checklist
- One logger module (
src/lib/logger.ts), one configuration - import the same instance everywhere. - JSON in prod, pretty in dev - flip on
NODE_ENV. LOG_LEVELenv var to change verbosity without code changes.- Redact secret-y keys (
authorization,cookie,password,token) globally. - Use
logger.child(...)to bind context; never string-concat fields into the message. - Pass
Errorobjects as{ err }- pino has a custom serialiser that captures stack + name + message. - Don’t
console.login server code ⚠️ - it’s synchronous, unstructured, and bypasses redaction.
Pino + Next.js / Edge runtime caveat
pino is a Node-only library - it won’t run on the Edge runtime (export const runtime = 'edge'). For Edge routes, use a lighter logger or write console.log(JSON.stringify({...})) directly. The same applies to Cloudflare Workers and Vercel Edge Functions.
Modules & Declarations
// Re-export pattern (barrel file: src/index.ts)
export { UserRepository } from './user-repository'
export type { User, AdminUser } from './types'
export * from './utils'
// Namespace import
import * as Utils from './utils'
// Dynamic import (code splitting)
const { heavy } = await import('./heavy-module')
// Ambient declarations (for untyped JS packages)
// src/types/globals.d.ts
declare module '*.svg' {
const content: string
export default content
}
declare global {
interface Window {
analytics: { track(event: string, props?: object): void }
}
}
// Augment an existing module
declare module 'express' {
interface Request {
user?: User
}
}Next.js - App Router
🛠️ Deeper reference - this section covers App Router layouts, dynamic pages, Server Actions, Route Handlers, middleware, and data-fetching patterns. It’s a mini-guide, not a quick-reference snippet.
Layouts and pages
// app/layout.tsx - root layout (server component)
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'My application',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}// app/users/[id]/page.tsx - dynamic page (server component)
import { notFound } from 'next/navigation'
interface Props {
params: { id: string }
searchParams: { tab?: string }
}
// Generate static params for SSG
export async function generateStaticParams() {
const users = await fetchUsers()
return users.map(u => ({ id: u.id }))
}
// Page-level metadata
export async function generateMetadata({ params }: Props) {
const user = await fetchUser(params.id)
return { title: user?.name ?? 'Not found' }
}
export default async function UserPage({ params, searchParams }: Props) {
const user = await fetchUser(params.id)
if (!user) notFound()
return <UserProfile user={user} tab={searchParams.tab} />
}Server Actions
// app/users/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user']),
})
export async function createUser(
prevState: { error?: string },
formData: FormData
) {
const parsed = CreateUserSchema.safeParse(Object.fromEntries(formData))
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors.name?.[0] ?? 'Invalid input' }
}
await db.user.create({ data: parsed.data })
revalidatePath('/users')
redirect('/users')
}// app/users/create/page.tsx - form using useFormState
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createUser } from '../actions'
function SubmitButton() {
const { pending } = useFormStatus()
return <button type="submit" disabled={pending}>{pending ? 'Creating...' : 'Create'}</button>
}
export default function CreateUserPage() {
const [state, action] = useFormState(createUser, {})
return (
<form action={action}>
<input name="name" required />
<input name="email" type="email" required />
<select name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
{state.error && <p>{state.error}</p>}
<SubmitButton />
</form>
)
}Route Handlers (API routes)
// app/api/users/[id]/route.ts
import { type NextRequest, NextResponse } from 'next/server'
export async function GET(
_req: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await fetchUser(params.id)
if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(user)
}
export async function PATCH(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await req.json() as Partial<User>
const updated = await updateUser(params.id, body)
return NextResponse.json(updated)
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: { id: string } }
) {
await deleteUser(params.id)
return new NextResponse(null, { status: 204 })
}Middleware
// middleware.ts (project root)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add response headers
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}Data fetching patterns
// Server component - fetch with cache control
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // ISR: revalidate every 60s
// next: { tags: ['posts'] } // on-demand revalidation via revalidateTag
// cache: 'no-store' // SSR: never cache
})
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
// Client component - SWR
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
export function PostList() {
const { data, error, isLoading } = useSWR<Post[]>('/api/posts', fetcher)
if (isLoading) return <p>Loading…</p>
if (error) return <p>Error: {error.message}</p>
return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}Environment Variables
// next.config.ts - validate at build time
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
AZURE_CLIENT_ID: z.string().uuid().optional(),
})
const env = envSchema.parse(process.env)
export default {
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
},
}
// Typed env module (src/env.ts)
export const serverEnv = envSchema.parse(process.env)Testing with Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
},
})// src/utils.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchUser } from './api'
vi.mock('./db', () => ({
db: { user: { findUnique: vi.fn() } },
}))
import { db } from './db'
describe('fetchUser', () => {
beforeEach(() => vi.clearAllMocks())
it('returns user when found', async () => {
const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' }
vi.mocked(db.user.findUnique).mockResolvedValue(mockUser)
const result = await fetchUser('1')
expect(result).toEqual(mockUser)
expect(db.user.findUnique).toHaveBeenCalledWith({ where: { id: '1' } })
})
it('returns null when not found', async () => {
vi.mocked(db.user.findUnique).mockResolvedValue(null)
const result = await fetchUser('999')
expect(result).toBeNull()
})
})# Run tests
pnpm vitest
pnpm vitest --coverage
pnpm vitest --ui # browser UI
pnpm vitest run # CI (no watch)CLI Tools & Command-Line Automation
Build a robust CLI with Commander.js
import { Command } from 'commander'
import pino from 'pino'
const logger = pino()
const program = new Command()
program
.name('deploy-tool')
.description('Infrastructure deployment automation')
.version('1.0.0')
// Global options
program
.option('-e, --environment <env>', 'deployment environment', 'dev')
.option('--dry-run', 'show what would happen without applying')
.option('-v, --verbose', 'verbose logging')
// Subcommands
program
.command('apply <config>')
.description('Apply deployment configuration')
.option('--force', 'skip confirmations')
.action(async (configFile: string, options) => {
const logLevel = program.opts().verbose ? 'debug' : 'info'
const env = program.opts().environment
logger.info({ configFile, env, dryRun: options.dryRun }, 'Starting apply')
try {
const config = await loadConfig(configFile)
await applyDeployment(config, env, options)
logger.info('Deployment successful')
} catch (error) {
logger.error(error, 'Deployment failed')
process.exit(1)
}
})
program
.command('rollback <version>')
.description('Rollback to a previous version')
.action(async (version: string) => {
logger.info({ version }, 'Rolling back')
await rollbackDeployment(version)
})
// Error handling
program.parseAsync(process.argv).catch((err) => {
logger.error(err, 'CLI error')
process.exit(1)
})Execute shell commands with type safety
import { execFile } from 'child_process'
import { promisify } from 'util'
const execFileAsync = promisify(execFile)
async function runCommand(
command: string,
args: string[],
timeout = 30000
): Promise<{ stdout: string; stderr: string }> {
try {
const result = await execFileAsync(command, args, {
timeout,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
})
return result
} catch (error) {
if (error instanceof Error && 'code' in error) {
const err = error as NodeJS.ErrnoException
logger.error({ command, args, code: err.code }, 'Command failed')
throw new Error(`Command '${command}' failed with code ${err.code}`)
}
throw error
}
}
// Usage: typed Azure CLI wrapper
async function getResourceGroups(subscription: string): Promise<string[]> {
const { stdout } = await runCommand('az', [
'group',
'list',
'--subscription',
subscription,
'--query',
'[].name',
'-o',
'json',
])
return JSON.parse(stdout)
}Infrastructure as Code (IaC) Patterns
AWS CDK with TypeScript (type-safe infrastructure)
import * as cdk from 'aws-cdk-lib'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as ecs from 'aws-cdk-lib/aws-ecs'
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'
interface AppStackProps extends cdk.StackProps {
environment: 'dev' | 'prod'
desiredCount: number
}
class AppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: AppStackProps) {
super(scope, id, props)
// VPC
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: props.environment === 'prod' ? 3 : 2,
natGateways: props.environment === 'prod' ? 2 : 1,
})
// ECS Cluster
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
containerInsights: true,
})
// Task Definition (EC2 launch type sets cpu/memory at the container level)
const taskDef = new ecs.Ec2TaskDefinition(this, 'TaskDef')
taskDef.addContainer('app', {
image: ecs.ContainerImage.fromAsset('./app'),
memoryLimitMiB: 512,
cpu: 256,
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'ecs',
}),
portMappings: [{ containerPort: 8080 }],
})
// ECS Service
const service = new ecs.Ec2Service(this, 'Service', {
cluster,
taskDefinition: taskDef,
desiredCount: props.desiredCount,
})
// Load Balancer
const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
vpc,
internetFacing: true,
})
const listener = alb.addListener('Listener', { port: 80 })
listener.addTargets('Targets', {
port: 8080,
targets: [service],
healthCheck: {
path: '/health',
interval: cdk.Duration.seconds(30),
},
})
// Outputs
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: alb.loadBalancerDnsName,
exportName: `${id}-alb`,
})
}
}
const app = new cdk.App()
new AppStack(app, 'app-dev', { environment: 'dev', desiredCount: 1 })
new AppStack(app, 'app-prod', { environment: 'prod', desiredCount: 3 })
app.synth()Container & Kubernetes Helpers
Health check for deployments
import http from 'http'
import pino from 'pino'
const logger = pino()
interface HealthCheckResult {
status: 'healthy' | 'degraded' | 'unhealthy'
timestamp: string
checks: Record<string, { status: string; message?: string }>
}
async function performHealthChecks(): Promise<HealthCheckResult> {
const checks: Record<string, { status: string; message?: string }> = {}
// Check database connection
try {
await db.$queryRaw`SELECT 1`
checks.database = { status: 'ok' }
} catch (error) {
checks.database = { status: 'failed', message: String(error) }
}
// Check external API. Note: native fetch has no `timeout` option - use AbortSignal.timeout().
try {
const response = await fetch('https://api.example.com/health', { signal: AbortSignal.timeout(5000) })
checks.externalApi = { status: response.ok ? 'ok' : 'failed' }
} catch (error) {
checks.externalApi = { status: 'failed', message: String(error) }
}
// Determine overall status
const failedChecks = Object.values(checks).filter((c) => c.status === 'failed').length
const status =
failedChecks === 0 ? 'healthy' : failedChecks < Object.keys(checks).length ? 'degraded' : 'unhealthy'
return {
status,
timestamp: new Date().toISOString(),
checks,
}
}
// HTTP health endpoint
http.createServer(async (req, res) => {
if (req.url === '/health') {
const health = await performHealthChecks()
const statusCode = health.status === 'healthy' ? 200 : health.status === 'degraded' ? 503 : 503
res.writeHead(statusCode, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(health))
} else {
res.writeHead(404)
res.end()
}
}).listen(8080)Configuration Management
Load and validate environment configuration
import { z } from 'zod'
// Define config schema
const configSchema = z.object({
NODE_ENV: z.enum(['dev', 'staging', 'prod']).default('dev'),
PORT: z.coerce.number().default(8080),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
AZURE_SUBSCRIPTION_ID: z.string(),
REPLICAS: z.coerce.number().default(1),
DRY_RUN: z
.string()
.transform((s) => s === 'true')
.default('false'),
})
type Config = z.infer<typeof configSchema>
let config: Config
export function loadConfig(): Config {
if (config) return config
const result = configSchema.safeParse(process.env)
if (!result.success) {
const errors = result.error.flatten()
throw new Error(`Configuration validation failed:\n${JSON.stringify(errors, null, 2)}`)
}
config = result.data
return config
}
// Usage
const cfg = loadConfig()
console.log(`Running in ${cfg.NODE_ENV} mode, replicas=${cfg.REPLICAS}`)Multi-environment configuration files
import * as fs from 'fs'
import * as path from 'path'
interface EnvConfig {
replicas: number
resources: { cpu: string; memory: string }
timeout: number
}
function loadEnvConfig(environment: string): EnvConfig {
const configPath = path.join(process.cwd(), `config.${environment}.json`)
if (!fs.existsSync(configPath)) {
throw new Error(`Config file not found: ${configPath}`)
}
const content = fs.readFileSync(configPath, 'utf-8')
return JSON.parse(content) as EnvConfig
}
// Usage with CDK
const env = process.env.ENVIRONMENT || 'dev'
const envConfig = loadEnvConfig(env)
new AppStack(app, `app-${env}`, {
environment: env as 'dev' | 'prod',
desiredCount: envConfig.replicas,
})Deployment & Health Checks
Blue-green deployment orchestration
import pino from 'pino'
import { execFileAsync } from './cli'
const logger = pino()
interface DeploymentConfig {
appName: string
version: string
environment: 'dev' | 'prod'
}
async function blueGreenDeploy(config: DeploymentConfig): Promise<void> {
logger.info({ config }, 'Starting blue-green deployment')
const { appName, version, environment } = config
try {
// 1. Determine current slot
const currentSlot = await getCurrentSlot(appName)
const targetSlot = currentSlot === 'blue' ? 'green' : 'blue'
logger.info({ currentSlot, targetSlot }, 'Determined deployment slots')
// 2. Deploy to inactive slot
await deployToSlot(appName, targetSlot, version)
// 3. Run health checks
const isHealthy = await runHealthChecks(appName, targetSlot)
if (!isHealthy) {
throw new Error(`Health checks failed on ${targetSlot} slot`)
}
// 4. Switch traffic
await switchSlots(appName, targetSlot)
logger.info({ targetSlot }, 'Deployment successful')
} catch (error) {
logger.error(error, 'Deployment failed, keeping previous version active')
throw error
}
}
async function runHealthChecks(appName: string, slot: string): Promise<boolean> {
const url = `https://${appName}-${slot}.azurewebsites.net/health`
const maxRetries = 5
let lastError: Error | null = null
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(10000) })
if (response.ok) {
const health = await response.json()
logger.info({ health }, 'Health check passed')
return true
}
} catch (error) {
lastError = error as Error
logger.warn({ attempt: i + 1, error: lastError.message }, 'Health check attempt failed')
await new Promise((r) => setTimeout(r, 2000 * (i + 1))) // exponential backoff
}
}
logger.error(lastError, 'All health check attempts failed')
return false
}
async function getCurrentSlot(appName: string): Promise<string> {
// List deployment slots; presence of an active "green" slot decides the target.
const { stdout } = await execFileAsync('az', [
'webapp',
'deployment',
'slot',
'list',
'--name',
appName,
'--resource-group',
'my-rg',
'--query',
"[?state=='Running'].name",
'-o',
'json',
])
const slots: string[] = JSON.parse(stdout || '[]')
return slots.includes('green') ? 'green' : 'blue'
}
async function deployToSlot(appName: string, slot: string, version: string): Promise<void> {
logger.info({ slot, version }, 'Deploying to slot')
// Implementation: push to Azure App Service deployment endpoint
}
async function switchSlots(appName: string, slot: string): Promise<void> {
logger.info({ slot }, 'Switching traffic to slot')
await execFileAsync('az', ['webapp', 'deployment', 'slot', 'swap', '--name', appName, '--resource-group', 'my-rg', '--slot', slot, '--target-slot', 'production'])
}
// Usage
await blueGreenDeploy({
appName: 'api-service',
version: 'v2.1.0',
environment: 'prod',
})Anti-patterns
- ⚠️
anytype - silently disables type checking for that value and everything downstream. Useunknownand narrow before use, or define the actual shape with an interface or type. - 🚨 Trusting unvalidated JSON from external sources -
res.json() as Useris a compile-time assertion, not runtime validation. Parse with Zod or a type guard before treating the value as typed. - ⚠️
console.login server-side code - synchronous, unstructured, and bypasses redaction. Use pino or another structured logger with levels. - 🔬 Untyped
fetchresponses - wrappingfetchwithas SomeTypedoes not validate the shape at runtime. Use a typed wrapper with Zod parsing or explicit type guards. - 🔬 Numeric
enum- numeric enums produce surprising reverse-mappings at runtime (Direction[0] === 'Up'). Preferconst enum(inlined, no runtime object) or discriminated unions for domain modelling. - 🔬 Ignoring settled results in
Promise.allSettled- iterating without checkingr.status === 'fulfilled'silently drops errors. Always handle bothfulfilledandrejectedcases. - 🔬 Barrel files with circular re-exports -
index.tsthat re-exports everything can create circular dependency chains and slow down tree-shaking. Import from the source file directly in performance-sensitive paths.