Skip to content

Type Safety & TypeScript

TypeScript Strict Mode — Non-Negotiable for Aidxn in 2026

All articles
🛡️ 📘 ✅

Why Every Velocity X Project Demands Strict Type Checking

## What Strict Mode Actually Means TypeScript has two personalities. Loose mode (default) assumes you might not know what you're doing — it's forgiving, lets undefined slide, allows implicit `any` types, doesn't complain about unchecked array access. It's TypeScript with training wheels. Strict mode removes the training wheels. It enforces explicit types at every step. No implicit `any`. No optional-chaining surprises. No accessing array indices that might not exist. If a value could be undefined, you handle it or TypeScript screams. Strict mode is not "better TypeScript" — it's a different language. You write less code, but every line you write is a guarantee. The compiler becomes your co-pilot. In 2026, every Velocity X project ships with strict + `noUncheckedIndexedAccess` enabled by default. Not as an option. As a requirement. This is the non-negotiable stance: loose TypeScript catches ~30% of bugs at compile time. Strict TypeScript catches ~90%. The remaining 10% are logic errors — wrong algorithm, not type errors. The difference between "hope the test catches it" and "the compiler won't let you ship it." ## What Strict Mode Enables (Real Protection) When you enable strict mode, TypeScript enables five sub-rules that compound into a fortress: **1. noImplicitAny** — every parameter and variable must have an explicit type or be inferrable. ```typescript // Loose: allowed. Runtime error when foo is undefined. function process(foo) { return foo.length; } // Strict: compile error. You must declare foo's type. function process(foo: string | undefined) { return foo?.length ?? 0; } ``` **2. strictNullChecks** — null and undefined are explicit. If a variable could be null, the type must say so. ```typescript // Loose: string | null is the same as string. No difference. const name: string = null; // Allowed. // Strict: this is a type error. name is string, not string | null. const name: string = null; // Error. const name: string | null = null; // Correct. ``` **3. strictFunctionTypes** — function parameters are contravariant. A function expecting a more general type can't be assigned to a function expecting a specific type. ```typescript // Loose: this type assignment is allowed. type Handler = (x: string | number) => void; const handler: (x: string) => void = (x) => console.log(x); const h: Handler = handler; // No error, but dangerous. // Strict: Error. handler only accepts strings, but Handler might pass numbers. ``` **4. strictPropertyInitialization** — every property must be initialized or explicitly marked optional. ```typescript // Loose: allowed. id might be undefined at runtime. class User { id: number; name: string; constructor(name: string) { this.name = name; // id is uninitialized but the type says number. } } // Strict: Error. Either initialize id or mark it optional or use a definer. class User { id?: number; name: string; constructor(name: string) { this.name = name; } } ``` **5. noImplicitThis** — inside a function, `this` must be explicitly typed. All five combine: variables have explicit types, null is explicit, functions don't silently break type contracts, class properties are always initialized, and `this` is never a mystery. The compiler enforces a contract. You don't write code, you sign a contract with the type system — and the compiler enforces it at compile time instead of runtime. On top of these, we add `noUncheckedIndexedAccess`. Access an array index that might not exist? Type error. ```typescript const items: string[] = ['a', 'b']; const first = items[0]; // Strict: first is string | undefined, not string. ``` This catches off-by-one errors, empty array assumptions, and loop-boundary bugs before they reach QA. ## Recommended tsconfig.json for Aidxn 2026 This is the config we ship with every Velocity X project. Annotated inline: ```json { "compilerOptions": { // Language and environment "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "module": "ESNext", // The fortress: strict mode "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, // Module resolution "moduleResolution": "bundler", "resolveJsonModule": true, "allowJs": true, "skipLibCheck": true, // Output and decorators "declaration": true, "declarationMap": true, "sourceMap": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, // Path aliases (required for monorepos and large projects) "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@lib/*": ["src/lib/*"], "@types/*": ["src/types/*"] } }, "include": ["src"], "exclude": ["node_modules", "dist", "build"] } ``` Key points: - **`strict: true`** enables all five rules. - **`noUncheckedIndexedAccess: true`** catches array index surprises. - **`noImplicitReturns: true`** enforces function return paths (all branches must return). - **`noFallthroughCasesInSwitch: true`** prevents accidental case fallthrough. - **`noImplicitOverride: true`** catches when you override a parent method and the signature changes. - **`skipLibCheck: true`** skips type-checking node_modules — saves compile time. - **`paths`** aliases reduce import hell. Use `@/` instead of `../../../`. Apply this to a new React + Vite project and you'll spend the first 30 minutes fixing type errors that would have been runtime bugs. After 30 minutes, you won't need a linter — the compiler is your linter. ## Four Real Bug Captures These are actual bugs we caught in client projects before they shipped: **1. Undefined Array Access (e-commerce checkout)** A checkout form was iterating over cart items. The loop accessed `cart.items[index].sku` without checking if the index existed. ```typescript // Loose: no warning. cart.items.forEach((item, index) => { const sku = cart.items[index].sku; // Redundant, but allowed. }); // Strict with noUncheckedIndexedAccess: // Error: cart.items[index] is string | undefined. ``` The actual bug: sometimes the array mutated during iteration (a background sync removed items). Strict mode forced a null check. We rewrote it to avoid iteration-time mutations. Bug prevented before any user saw it. **2. Optional Property Not Checked (dashboard widget)** A dashboard component expected an optional `user.premium` boolean. In loose mode, TypeScript allowed accessing `.premium` without checking if `user` was undefined. ```typescript // Loose: allowed, but crashes if user is undefined. const isPremium = user.premium; // No warning. // Strict: Error. user might be undefined, and undefined.premium is an error. const isPremium = user?.premium ?? false; // Correct. ``` The bug happened when a user's session expired but the component re-rendered anyway. Strict mode forced us to handle the missing user case. We added a loading state. No more crash. **3. Unchecked Object Keys (form state manager)** A form reducer was accessing state properties dynamically. In loose mode, this silently returned undefined. ```typescript // Loose: allowed. returns undefined if key doesn't exist. const value = formState[fieldName]; // fieldName could be anything. // Strict: Error. formState[fieldName] is a | b | undefined. const value = formState[fieldName as keyof typeof formState]; ``` The bug: a typo in a field name meant data wasn't being saved. Strict mode caught the type mismatch and forced us to add runtime validation. The form now validates field names at parse time. **4. Missing Return Path (async utility)** A utility function returned a user object or threw an error — except one path didn't return anything. ```typescript // Loose: allowed, function returns undefined sometimes. function getUser(id: string): User { if (id === 'guest') throw new Error('...'); if (id === 'admin') return admin; // Missing return for other ids. } // Strict with noImplicitReturns: Error. Not all code paths return a value. ``` The bug was a logic error — the function forgot to return for regular users. Strict mode caught it before the undefined reached the call site and broke the UI. We fixed the missing return in 30 seconds instead of debugging it in production. All four bugs would have shipped in loose mode and hit QA or production. Strict mode caught them at compile time, zero runtime cost. ## Migration from Loose to Strict TypeScript If you're on a loose codebase, the migration is straightforward but tedious. **Phase 1: Enable strict mode (don't panic)** ```json { "compilerOptions": { "strict": true } } ``` Run `tsc --noEmit`. You'll see 500+ errors. This is normal. They're all real bugs, not TypeScript yelling at you for no reason. **Phase 2: Prioritize by file** Start with the most-used files first (components, utilities, lib). Fix the files that unblock the most other files. TypeScript will tell you which errors cascade. **Phase 3: Add explicit types** Most errors are "implicit any" — add types to function parameters and return values. ```typescript // Before const processOrder = (order) => { ... } // After const processOrder = (order: Order): Promise=> { ... } ``` **Phase 4: Add null checks** Where functions or API calls return optional values, add null checks: ```typescript // Before const user = await getUser(id); return user.name; // After const user = await getUser(id); if (!user) throw new Error('User not found'); return user.name; ``` **Phase 5: Fix callback types** Functions passed as callbacks often need explicit parameter types: ```typescript // Before items.map(item => item.id) // After items.map((item: Item) => item.id) ``` On a medium codebase (100 files), this takes 2–4 days. The payoff: every new feature is type-safe and zero runtime errors related to type mismatches. ## Six FAQs **Q: Does strict mode slow down TypeScript compilation?** A: Negligibly. Strict mode doesn't add compilation cost — it just catches more errors. If anything, a codebase gets faster because you're not chasing undefined bugs later. **Q: Can I enable strict mode gradually, file by file?** A: Not really. Strict mode is a project-wide setting. But you can use `// @ts-ignore` on specific lines while migrating. Not ideal, but it unblocks the transition. **Q: Does strict mode work with external libraries that are loosely typed?** A: Yes. If a library is untyped or loosely typed, you can wrap it with your own types. We did this for a weather API that returned `any` — we added a validator and typed the response explicitly. **Q: Is noUncheckedIndexedAccess too strict? It flags every array access.** A: Yes, it's strict. But that's the point. If you're accessing an array index that might not exist, you should handle it. Most of the time, the fix is one line: `items[index]?.property ?? fallback`. **Q: How do I deal with third-party data that doesn't match types?** A: Validate at the boundary. When data enters your app (API response, form input), validate it against a Zod schema. Inside your app, trust the types — Zod guarantees it. ```typescript const UserSchema = z.object({ id: z.number(), name: z.string() }); const user = UserSchema.parse(apiResponse); // Now user is typed and safe. ``` **Q: Can I commit to strict mode if my team is new to TypeScript?** A: Yes, but expect a ramp-up. The first week is hard. The second week is easier. By week four, strict mode feels normal. We've done this with teams of 3–8 people and it always works — the compiler teaching moments pay for themselves. ## The Verdict Loose TypeScript is a footgun. It gives you the syntax of type safety without the actual safety. Strict mode removes the syntax sugar and replaces it with real guarantees. In 2026, we don't ship Velocity X projects without strict + `noUncheckedIndexedAccess`. Not because the project is large or the team is experienced. Because strict mode catches bugs at compile time instead of runtime, and the compiling cycle is free. Every hour spent fixing a type error is an hour not spent in production debugging. The four bugs we caught above would have shipped, been caught by QA, been debugged in prod, or hit customers. Strict mode prevented all of them before any human eyes saw the code. That's not theoretical — it's measurable, and it compounds across a year of development. If you're building a modern web app in 2026, strict TypeScript is not optional. Check out our pricing page to see how we build type-safe systems at scale — we document our approach to type safety and testing infrastructure there. For a deeper look at how strict mode integrates with modern build tooling, read our Vite vs Webpack post, where we cover type-checking in the context of dev-server architecture. The best type system is the one that doesn't let you make mistakes.
Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.