TypeScript only pays off when it is strict at the boundaries and boring in the middle. Most production bugs we see in Saudi SaaS and stores are not algorithm errors. They are missing awaits on payments, any leaking from CMS payloads, unvalidated webhooks, and nullable Arabic fields assumed present.
This is the exact rule set we enforce to keep deploys calm.
1. Strict config that catches real bugs
Enable strict true, noUncheckedIndexedAccess true, noImplicitAny true, and exactOptionalPropertyTypes where feasible. Ban any via eslint no-explicit-any. Require explicit return types on exported functions. These three catch the classic crash of order.items[0].price when items is empty, which happens with Arabic test orders that have free samples.
Add no-floating-promises and await-thenable. A missing await on a Moyasar capture call causes double charges when the user double-clicks Apple Pay. Lint must fail the build, not warn.
Keep tsconfig in the repo root with paths for @/lib. Do not allow ts-ignore without a ticket link. Track ignore count in CI and fail if it grows.
2. Validate at the edges with Zod
Every external input gets a Zod schema. Fatoora XML mapped to JSON, payment webhooks, Directus or CMS webhooks, upload metadata, and Arabic form fields with phone numbers. Parse, do not cast. Use z.object with strict shapes, transform phone numbers to E.164, and coerce dates with timezone Asia/Riyadh.
Example pattern is webhook schema with paymentId string, amount number positive, currency literal SAR, and status enum. On failure log the flattened Zod error with provider and IP, return 400, and alert if failures spike. This turns silent data corruption into visible 400s you can fix in mapping.
Generate API types from your backend schema or OpenAPI rather than hand-writing duplicates. For CMS content, generate types for articles plus translations with nullable excerpt. This removes any from content rendering, where Arabic fallback logic lives.
3. Narrow types for bilingual content
Model locale as union ar-SA plus en-US, not string. Model translation status as complete or missing-ar or missing-en. Functions that render must accept resolved content with guaranteed title, not raw nullable rows. Write a resolveLocale function that takes requested locale and rows and returns content with fallback plus a flag. Exhaustiveness check with never for switch on status ensures new locales force updates.
Prefer const objects with unions over enums for public APIs. Enums emit JS that complicates edge runtimes. Const plus typeof union is leaner for Next.js edge.
4. Errors, results, and money paths
Payments and invoicing must use Result patterns, not throw for expected failures. Return ok with value or err with code like INSUFFICIENT_FUNDS or ZATCA_REJECTED with rule code. The caller decides retry versus user message in Arabic. Throw only for programmer errors.
Wrap idempotency keys for Moyasar captures and ZATCA clearance retries. Same key plus same payload returns stored result. Different payload with same key is a bug and must alert.
Log in structured JSON with requestId, userId, orderId, lang, and provider code. Arabic messages in logs must be UTF-8, searchable by order number. This cuts support time from hours to minutes during White Friday.
5. Testing types, not just code
Add type tests with expectTypeOf for critical mappers like UBL builders and webhook parsers. Add property tests for VAT rounding with random SAR amounts to catch half-up versus bankers rounding mismatches that ZATCA rejects.
Review diffs under 400 lines. Require reproduction steps for type fixes. Keep a living ADR for why each strict flag exists, with incident links. New hires then respect rules instead of disabling them.
Bottom line is strict plus validated edges plus explicit bilingual types. These small rules prevent 80 percent of payment, webhook, and RTL incidents we see in production.





