Skip to main content
TypeScriptNext.jsEngineering

Mastering TypeScript Domain-Driven Typing: Beyond Simple Interfaces

elango
elango

Full Stack Engineer

Updated
5 min read
Mastering TypeScript Domain-Driven Typing: Beyond Simple Interfaces

When engineers talk about TypeScript excellence they often assume every file is .ts with strict mode enabled repo-wide. My elangodev.com codebase tells a different story — and a more common one for shipping products. Most App Router pages and API routes are JavaScript for velocity, while critical configuration and tooling use TypeScript where compile-time guarantees pay rent. Domain-driven typing, for me, means encoding business rules in types at the boundaries that matter, not converting 200 JSX files overnight.

Why I Type the Config Layer First

next.config.ts is the spine of my deployment. A typo in experimental flags or image remote patterns breaks production builds silently if left unchecked. Importing NextConfig gives me autocomplete for supported keys and prevents invalid nested objects from slipping through code review.

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
  experimental: {
    optimizePackageImports: ["lucide-react", "framer-motion"],
    scrollRestoration: true,
  },
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "elangomedia.s3.ap-southeast-2.amazonaws.com" },
    ],
    formats: ["image/avif", "image/webp"],
  },
};
export default nextConfig;

That file documents domain constraints — which S3 hostnames are trusted, which bots receive full HTML — in a typed structure I cannot mistype without the compiler complaining.

JSDoc as a Bridge for JavaScript Modules

Shared utilities like blog word-count validation live in JavaScript but still expose typed contracts through JSDoc. My AdSense readiness scripts import wordCount and meetsMinWordCount from lib/blogContent.js knowing exactly what each function accepts. Components like AdUnit.jsx annotate props inline so editors surface misuse without converting every client component.

// components/AdUnit.jsx — prop contract without TS syntax
/**
 * @param {string} slot - The ad slot ID from AdSense
 * @param {string} format - Ad format (default: 'auto')
 * @param {boolean} responsive - Whether the ad is responsive
 * @param {string} className - Additional CSS classes
 */

This is domain-driven typing at the monetization boundary: ad slots are strings tied to AdSense inventory, formats are enumerated strings, and responsive defaults are explicit. The pattern scales to API route helpers where I document Supabase row shapes until I promote modules to TypeScript.

Modeling Blog Domain Rules in Code

My AI blog generator enforces content quality through domain constants, not scattered magic numbers. MIN_BLOG_GENERATION_WORDS lives beside wordCount and stripHtml so both the Gemini route and AdSense audit scripts share one definition of "long enough." When I rewrite seed posts in scripts/content/rewritten/, I validate against the same helper the production cron uses.

The generator route also defines a response schema object matching Gemini structured output — title, slug, excerpt, content, tags — before inserting into Postgres. That schema is the domain model for published posts. Slugs must be URL-safe; tags must be arrays; content must exceed the word threshold. Treating those as invariant rules at the write boundary prevents thin AI drafts from reaching the handbook index.

Branded Types and Union States in Practice

I apply discriminated union thinking in React state even in JS files. Chat connection status uses explicit strings like CONNECTING and SUBSCRIBED rather than boolean soup. Message edit mode tracks either null or a full message object, never half-populated structs. When I migrate modules to TypeScript, those patterns map cleanly to union types without refactors.

Template literal types shine in route naming — I keep slugs kebab-case in the database and generate canonical URLs in metadata helpers. A future type BlogSlug = typeof publishedSlugs[number] would lock links to known posts; today I enforce that through database uniqueness and seed scripts.

Migration Strategy for This Repo

I add TypeScript at the highest-leverage edges first: config, shared lib contracts, and payment or auth utilities if they grow. UI components stay JavaScript until a file becomes bug-prone. Domain-driven typing is about protecting invariants — admin email checks, word counts, image size caps — not about winning syntax debates.

When I introduce a new environment variable for production, I add it to my Vercel project settings and document the expected type in comments beside the read site. Missing env vars fail builds early on blog generation routes rather than silently publishing broken cover images. That discipline mirrors branded types: configuration is part of the domain, not an afterthought.

If you maintain a Next.js site similar to mine, start by typing configuration and shared validation modules, document JavaScript props with JSDoc, and encode business enums as explicit string constants. That delivers most of TypeScript safety without blocking feature work on frontend experiments.

I also run npm run check:adsense locally, which imports the same word-count helpers as production generators. Sharing typed contracts between scripts and API routes caught a drift bug where two files used different minimum word thresholds. Domain-driven typing is ultimately about one source of truth for business numbers — word counts, image size caps, admin email — regardless of file extension. That is the typing philosophy I export to client repos even when they remain JavaScript-first. Types are how I encode policy, not how I impress interviewers. When a PR changes a domain constant, every consumer must compile or lint before merge — that is the real safety net on a mixed JS and TS codebase like mine.

elango

Written by

elango

Full Stack Engineer & Software Architect specializing in React, Next.js, and high-performance web applications.

Share on:

Discover More

View blog