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.
Types that mirror elangodev.com domains
Posts, handbook sections, and AdSense readiness checks each have shapes I refuse to type as any. A Post on this site is not a generic CMS blob — it has slug, status, excerpt, tags, and HTML content with invariants enforced before publish scripts mark status: published.
type PostStatus = 'draft' | 'published';
type Post = {
slug: string;
title: string;
excerpt: string;
content: string;
status: PostStatus;
tags: string[];
};Domain-driven typing here means illegal states are hard to represent: you should not be able to call getFeaturedPosts and accidentally include drafts because the query and the type agree on status: 'published'.
Nominal typing for IDs
Mixing userId and postId as string caused one dashboard bug where I fetched the wrong row. Branded types (or opaque aliases) make that a compile error. I do not brand every string — only IDs that cross API boundaries.
What I skip
Over-modeling Zod schemas for static marketing props wastes time. I apply runtime validation at trust boundaries: API routes, webhook payloads, and admin forms. RSC props from my own SQL stay typed at compile time without double parsing.
Editorial scripts as typed contracts
scripts/rewrite-original-content.js and originality heuristics consume post objects. Keeping TEMPLATED_SEED_SLUGS and SKIP_PUBLISH_SLUGS as Set<string> with shared modules prevents drift between unpublish and rewrite jobs — a TypeScript-in-spirit discipline even when those scripts remain CommonJS for Node convenience.
Readiness checks as a typed domain
lib/adsense-readiness/checks.js encodes publisher quality as structured check objects with status PASS|FAIL|WARN. Treating that as a domain — not a pile of console.logs — made it possible to score content depth separately from robots configuration. The same idea applies to product code: name the invariant, then type it.
I raised thin-content thresholds so a “green” readiness report cannot claim success at 600-word posts. Types and thresholds together stop us from lying to ourselves before the next AdSense submit.
Worked example: publish pipeline
The publish pipeline refuses to mark a post published when originality heuristics score it below the keep threshold or when word count is under the readiness floor. Those rules live next to the types so product and scripts agree. I would rather fail a seed job than ship another templated tutorial into the indexed corpus.
Domain typing without enforcement is documentation cosplay. Enforcement without clear types becomes tribal knowledge in one person’s head. This site aims for both.
Reader outcomes
After reading this guide on elangodev.com you should be able to implement the pattern in a real Next.js codebase, explain the trade-offs to a teammate, and know which failure modes I already hit in production. That outcome-focused bar is how I decide whether an article is done — not a round word count alone, though we now target roughly 1,500 words of substantive detail for keepers in the AdSense recovery corpus.





