Skip to Content
CheatsheetsTypeScript

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

Bash
# 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 22

Project bootstrap

Bash
# 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 --watch
JSON
{
  "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

Bash
# 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

TypeScript
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

TypeScript
// 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] = point

Objects and interfaces

TypeScript
// 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

TypeScript
// 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

TypeScript
// 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

TypeScript
// 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 : T

Utility Types

TypeScript
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

TypeScript
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

TypeScript
// 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

TypeScript
// 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, AsyncLocalStorage for 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)

Bash
pnpm add pino
pnpm add -D pino-pretty   # dev-only - human-readable output
TypeScript
// 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:

TypeScript
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 objects

Child loggers - bind context once, reuse everywhere

TypeScript
// 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.

TypeScript
// 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
TypeScript
// 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.

TypeScript
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:

TypeScript
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

LevelUse for
tracePer-iteration detail, payload dumps - dev only
debugLocal diagnostics - off in prod
infoBusiness events: request handled, job completed
warnRecoverable issue, retry succeeded, fallback used
errorAn operation failed; caller will see a failure
fatalProcess 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_LEVEL env 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 Error objects as { err } - pino has a custom serialiser that captures stack + name + message.
  • Don’t console.log in 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

TypeScript
// 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

TypeScript
// 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>
  )
}
TypeScript
// 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

TypeScript
// 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')
}
TypeScript
// 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)

TypeScript
// 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

TypeScript
// 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

TypeScript
// 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

TypeScript
// 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

TypeScript
// 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'],
    },
  },
})
TypeScript
// 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()
  })
})
Bash
# 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

TypeScript
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

TypeScript
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)

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

  • ⚠️ any type - silently disables type checking for that value and everything downstream. Use unknown and narrow before use, or define the actual shape with an interface or type.
  • 🚨 Trusting unvalidated JSON from external sources - res.json() as User is a compile-time assertion, not runtime validation. Parse with Zod or a type guard before treating the value as typed.
  • ⚠️ console.log in server-side code - synchronous, unstructured, and bypasses redaction. Use pino or another structured logger with levels.
  • 🔬 Untyped fetch responses - wrapping fetch with as SomeType does 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'). Prefer const enum (inlined, no runtime object) or discriminated unions for domain modelling.
  • 🔬 Ignoring settled results in Promise.allSettled - iterating without checking r.status === 'fulfilled' silently drops errors. Always handle both fulfilled and rejected cases.
  • 🔬 Barrel files with circular re-exports - index.ts that re-exports everything can create circular dependency chains and slow down tree-shaking. Import from the source file directly in performance-sensitive paths.
Last updated on