TypeScript Strict Mode — Missing Null Check Broke Payments
Orders completed with zero amounts — a missing null check on user.
- TypeScript adds static types to JavaScript — catches bugs at write-time, not runtime
- Interface describes object shapes; type alias handles unions and intersections
- Strict mode (strict: true) is non-negotiable for production safety
- Generics give you reusable, type-safe abstractions without sacrificing flexibility
- The biggest mistake: using
anyas a crutch — it disables the type system and invites runtime failures
Imagine you're building with LEGO. Plain JavaScript is like a box of mixed bricks with no labels — you can grab any piece and try to snap it anywhere, only discovering it doesn't fit after you've built half the castle. TypeScript is like a labelled LEGO kit: every bag is marked, every piece has a designated slot, and the instructions tell you before you even start if you're trying to put a wheel where a window should go. The 'mistake' is caught before the castle collapses.
JavaScript runs half the internet, but it has a well-known Achilles heel: it will happily let you pass a number where a string is expected, call a method on undefined, or silently return NaN and keep going. These bugs don't blow up at write-time — they blow up at 2am in production, after a user has already seen broken behaviour. That's not a flaw in your skill; it's a flaw in the language's design philosophy. JavaScript was built for small scripts, not 100,000-line enterprise applications.
TypeScript is Microsoft's answer to that problem. It's a strict superset of JavaScript — meaning every valid JS file is already valid TypeScript — that adds a static type system on top. The TypeScript compiler reads your types, checks your logic against them, then compiles everything down to plain JavaScript that any browser or Node.js process can run. The types vanish at runtime; they exist purely to protect you at development time. Think of them as scaffolding: essential while you're building, removed once the structure is sound.
By the end of this article you'll understand not just what TypeScript syntax looks like, but why the type system is designed the way it is, how to use interfaces and generics to model real-world data structures, and which mistakes will waste your time if you don't know about them upfront. You'll walk away ready to migrate a JavaScript project to TypeScript or start a new one with confidence.
Static Typing vs Dynamic Typing — Why TypeScript Exists at All
In JavaScript every variable is dynamically typed — its type is determined at runtime based on the value it holds at that moment. That flexibility is useful for tiny scripts. But in a large codebase, it means you're constantly holding a mental model of what every variable 'should' be, because the language won't enforce it for you. Functions accept any argument, return values are ambiguous, and refactors become terrifying guessing games.
TypeScript introduces static typing: you declare what type a value is when you write the code, and the compiler verifies every usage before the code ever runs. If you write a function that expects a User object and you accidentally pass a plain string, TypeScript tells you immediately — right in your editor, with a red squiggle and a precise error message.
This isn't just about catching typos. It's about making your code self-documenting. A function signature like function sendEmail(recipient: User, subject: string): Promise<void> tells every developer on your team exactly what it needs and what it returns, with zero ambiguity. No more digging through documentation or console-logging to figure out what shape a value is. The types are the documentation.
.d.ts declaration file — avoid using any or the library will be untypedInterfaces and Type Aliases — Modelling Real-World Data Structures
Once you have more than a handful of typed variables, you need a way to describe the shape of complex objects — things like API responses, database records, or configuration objects. TypeScript gives you two tools for this: interface and type. They look similar and are often interchangeable, but they have meaningful differences worth understanding.
An interface is specifically designed to describe the shape of an object. It can be extended (like class inheritance), merged across multiple declarations, and implemented by a class. A type alias is more general — it can represent primitives, unions, intersections, tuples, and objects. If you're modelling an object that other parts of your code will implement or extend, reach for interface. If you need a union type or something more compositional, reach for type.
In a real project you'll use both. An API response might come back as User | null — that's a union, so type is appropriate. The User shape itself is a contract that components and services must honour — that's interface territory. Understanding when each tool is the right fit is what separates TypeScript beginners from developers who write genuinely maintainable typed code.
interface for object shapes that describe a 'thing' in your domain (User, Product, Order). Use type when you need unions (A | B), intersections (A & B), or to alias a primitive. When in doubt, interface is more extensible and gives better error messages.Generics — Writing Code That Works for Any Type Without Losing Safety
Here's a problem you hit quickly in TypeScript: you want to write a reusable utility function — say, one that wraps an API response — but you don't want to lose type safety by falling back to any. If you type the parameter as any, TypeScript stops checking it and you've thrown away all the benefits you came here for.
Generics solve this elegantly. A generic is a type placeholder — a variable for a type rather than a value. You write the function once with a generic parameter like <T>, and TypeScript figures out what T actually is at each call site based on what you pass in. You get full type safety AND full reusability. It's one of the features that genuinely changes how you think about writing abstractions.
Generics show up everywhere in real TypeScript codebases: API client wrappers, data transformation utilities, React component props, custom hooks, and state management patterns. Once you're comfortable with them, you'll find yourself reaching for them constantly instead of duplicating code or compromising on any.
response.json() as T, TypeScript believes you — it doesn't actually validate that the JSON matches T at runtime. If your API returns a different shape than expected, TypeScript won't catch it. For production apps, use a runtime validation library like Zod alongside TypeScript to validate API responses before you use them.any inside a generic function defeats the purpose — you lose all type safety.any in a generic constraint; prefer unknown and narrow the type explicitly.tsconfig.json — The Settings That Make TypeScript Actually Strict
Installing TypeScript and adding a few type annotations doesn't automatically make your code safe. TypeScript has a spectrum of strictness controlled by tsconfig.json, and the default settings are surprisingly permissive. The most important flag is strict: true — it's actually a shorthand that enables a bundle of individual checks, the most consequential being strictNullChecks.
Without strictNullChecks, TypeScript allows null and undefined to be assigned to any type. That means a variable typed as string could actually be null at runtime, and TypeScript won't complain. With it enabled, null and undefined become their own distinct types. A function that might return nothing must say so explicitly with a return type like string | null, and every caller must handle that null case. This is uncomfortable at first and absolutely worth it.
Other key flags: noImplicitAny prevents TypeScript from silently falling back to any when it can't infer a type — you're forced to be explicit. noUnusedLocals keeps your codebase clean. target and module control what JavaScript version TypeScript compiles down to. Starting a new project? Set strict: true from day one. Migrating an existing project? Enable flags incrementally to avoid being overwhelmed.
strict: false, add noImplicitAny: true alone, and fix those errors first. Then add strictNullChecks: true and fix those. One flag at a time makes the migration manageable and gives your team concrete progress milestones.Type Narrowing and Type Guards — Writing Code That Handles Multiple Types Safely
Once you start using union types (e.g., string | null, Success | Failure), you'll hit a wall: TypeScript won't let you access properties or call methods that only exist on one branch of the union. You need to narrow the type before you can use it. Type narrowing is the process of refining a broad type into a more specific one based on runtime checks.
TypeScript understands common JavaScript patterns as type guards: typeof, instanceof, in, equality checks, and user-defined predicates. When you write if (typeof value === 'string'), TypeScript knows that inside that if block, value is a string — and it can safely let you call .toLowerCase(). Outside the if, it's still the union type.
User-defined type guards are a powerful pattern for complex checks. A function that returns a predicate like value is string tells TypeScript that when the function returns true, the argument is that specific type. This is invaluable for validating API responses, discriminating unions, and parsing untrusted data.
kind or status (e.g., { kind: 'success', data: T } | { kind: 'error', error: string }). Then you can narrow with a simple if (response.kind === 'success') — it's cleaner and more maintainable.The Missing Null Check That Crashed Payment Processing
processPayment function had a user.phoneNumber access without a null check. With strictNullChecks disabled, phoneNumber could be undefined, but TypeScript treated it as string. The subsequent toUpperCase() call returned undefined, and the payment gateway received an invalid customer ID, silently failing.strict: true in tsconfig.json. This immediately surfaced 47 null-safety errors. Added explicit null checks and used optional chaining (user?.phoneNumber?.toUpperCase()) and default values. The payment function now returns a typed union PaymentResult | PaymentError.- TypeScript without strictNullChecks gives you ~30% of the safety you paid for. Always enable strict: true from day one.
- A clean compile does not mean correct runtime behavior — types are only as good as the constraints you enforce.
- Use code reviews to check for use of
anyand missing null handling. Automate this with ESLint rules.
tsc --noEmit to get exact line. Use typeof or keyof to inspect types in your editor. If the property exists at runtime but not in type, consider using a type assertion or updating the interface.noImplicitAny in tsconfig. Use generics if the type is unknown but should be constrained. Avoid any - prefer unknown and type narrowing.?.), nullish coalescing (??), or explicit checks (if (value !== undefined)) to narrow the type before assignment.Key takeaways
compiler flag is not optional for serious projects — without it (especially without strictNullChecks`) you're getting maybe 30% of TypeScript's actual safety benefits.any when you need flexibility without sacrificing correctness.interface is a contract for an object's shape and is the right default for domain models. A type alias is more powerful for unions, intersections, and compositional patternsCommon mistakes to avoid
4 patternsUsing `any` as an escape hatch everywhere
any with unknown when the type is genuinely unknown. With unknown TypeScript forces you to narrow the type before using it (e.g., if (typeof value === 'string')) rather than silently trusting you. Reserve any only for truly unavoidable third-party interop, and enable the noImplicitAny compiler flag so TypeScript flags accidental any usages.Conflating `interface` extending with `type` intersections and getting confused by error messages
& expecting them to behave like an interface extension, but conflict errors are cryptic and hard to debug.interface and extends. Use & intersections for mixing in utility shapes like { createdAt: Date } onto an existing interface.Forgetting that TypeScript types are erased at runtime
if (myValue instanceof MyInterface) expecting it to work, but you get a runtime error because interfaces don't exist in compiled JavaScript.typeof, instanceof for classes, or custom guard functions with value is MyType return types), or use a schema validation library like Zod which generates both the TypeScript type AND a runtime validator from a single schema definition.Not enabling strictNullChecks in production projects
strict: true in tsconfig.json. This enables strictNullChecks along with other strict flags. Add explicit null checks everywhere. Use optional chaining and nullish coalescing to handle undefined paths gracefully.Interview Questions on This Topic
What is the difference between `interface` and `type` in TypeScript, and when would you choose one over the other?
interface is designed for extensibility (supports declaration merging and extends) while type is more general (can represent unions, intersections, tuples). Use interface for domain models that other code will inherit or implement. Use type for unions (A | B), intersections (A & B), or when you need a computed type. Interfaces give better error messages when merging conflicts occur.Frequently Asked Questions
That's TypeScript. Mark it forged?
5 min read · try the examples if you haven't