Senior 15 min · March 05, 2026

TypeScript Strict Mode — Missing Null Check Broke Payments

Orders completed with zero amounts — a missing null check on user.phoneNumber caused payment failures.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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 any as a crutch — it disables the type system and invites runtime failures
✦ Definition~90s read
What is Introduction to TypeScript?

TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It exists because JavaScript’s dynamic typing — where variable types are determined at runtime — leads to a class of bugs that are expensive to catch and fix in production.

Imagine you're building with LEGO.

TypeScript adds a compile-time type system that catches type mismatches, null reference errors, and shape violations before your code ever runs. The strict mode you’ll read about in this article is the full enforcement of that type system: it turns on all the compiler flags that make TypeScript actually prevent the kind of null check omission that took down a payments pipeline.

Without strict mode, TypeScript is just JavaScript with optional annotations — useful, but not bulletproof. With it, you get a type checker that refuses to compile code that might access a property on undefined or pass a string where a number is expected.

This is the difference between a linter that suggests fixes and a compiler that enforces contracts. TypeScript isn’t the only option here — Flow (Facebook’s type checker) and pure JSDoc annotations exist — but TypeScript dominates because it integrates with every major editor, has the richest type inference engine, and its strict mode is the industry standard for production-grade JavaScript.

When you’re modeling payment data, user profiles, or API responses, interfaces and type aliases let you define the exact shape of your data structures — and the compiler will scream if you try to pass a malformed object. Generics extend that safety to functions and classes that work across multiple types without sacrificing type information.

Utility types like Pick, Omit, and Partial let you derive new types from existing ones without rewriting definitions. This article walks through each of those features with a concrete failure — a missing null check that broke payments — to show you exactly where the safety net fails and how strict mode patches it.

Plain-English First

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.

What TypeScript Strict Mode Actually Enforces

TypeScript strict mode is a compiler flag that enables a set of type-checking rules designed to catch null safety and implicit any violations at compile time. It activates strictNullChecks, noImplicitAny, strictFunctionTypes, and others, turning the type system from a loose annotation layer into a sound null-safety guard. Without it, null and undefined are assignable to any type, which silently permits runtime null pointer exceptions in production.

When strict mode is on, every variable, parameter, and return type must explicitly account for null or undefined using union types like string | null. This forces developers to handle missing values at every boundary — function calls, API responses, database queries. The compiler rejects code paths where a nullable value is used without a check, effectively making null safety a compile-time guarantee rather than a runtime hope.

Use strict mode in every TypeScript project from day one. In payment systems, a missing null check on a transaction amount or user ID can cause silent failures, charge errors, or data corruption. Strict mode eliminates entire categories of bugs that slip into production, especially in data-intensive flows where null values are common. It's not optional — it's the baseline for any system that processes money or user data.

Strict Mode Is Not Retroactive
Enabling strict mode on an existing codebase will surface hundreds of errors — plan a phased migration with type assertions only as temporary escapes.
Production Insight
A payment service processed refunds without strictNullChecks — a null transactionId from a failed DB query was passed to the refund API, causing a double charge.
Symptom: intermittent duplicate refunds with no error logs, only discovered during reconciliation.
Rule of thumb: any value that crosses a system boundary (API, DB, queue) must be typed as nullable unless proven non-null at compile time.
Key Takeaway
Strict mode makes null safety a compile-time guarantee, not a runtime hope.
Always enable strict mode in new projects — it prevents entire classes of production bugs.
For existing codebases, migrate incrementally with strict-mode per file using // @ts-strict comments.
TypeScript Strict Mode — Missing Null Check Broke Payments THECODEFORGE.IO TypeScript Strict Mode — Missing Null Check Broke Payments Key concepts and pitfalls in TypeScript strict mode strict: true in tsconfig Enables all strict type-checking options noImplicitAny Forces explicit type annotations strictNullChecks Catches null/undefined access errors Interfaces & Type Aliases Model real-world data shapes Generics & Utility Types Reusable, type-safe code patterns ⚠ Missing null check on payment amount caused runtime crash Always use strictNullChecks and optional chaining THECODEFORGE.IO
thecodeforge.io
TypeScript Strict Mode — Missing Null Check Broke Payments
Introduction Typescript

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.

userGreeting.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// --- WITHOUT TypeScript (plain JavaScript behaviour) ---
// This function has no idea what 'user' looks like.
// Call it wrong and you get a silent runtime bug.
function greetUserJS(user) {
  // If 'user' is undefined, this crashes at runtime — not at write-time
  return `Hello, ${user.firstName}!`;
}

console.log(greetUserJS({ firstName: 'Alice' }));  // Hello, Alice!
console.log(greetUserJS(undefined));               // 💥 TypeError at runtime


// --- WITH TypeScript ---
// We define the exact shape a User must have.
interface User {
  firstName: string;
  lastName: string;
  email: string;
}

// The function now ONLY accepts a value that matches the User shape.
// TypeScript will refuse to compile if you pass anything else.
function greetUser(user: User): string {
  // 'user.firstName' is guaranteed to be a string — no null checks needed here
  return `Hello, ${user.firstName} ${user.lastName}!`;
}

const alice: User = {
  firstName: 'Alice',
  lastName: 'Nguyen',
  email: 'alice@example.com',
};

console.log(greetUser(alice)); // Hello, Alice Nguyen!

// Trying to call with wrong data — TypeScript catches this BEFORE you run the code:
// greetUser('Alice');  
// Error: Argument of type 'string' is not assignable to parameter of type 'User'
Output
Hello, Alice Nguyen!
Why This Matters in Production:
The JavaScript version of this bug would only surface if a user actually triggered that code path in production. The TypeScript version surfaces the moment you save the file. That's the entire value proposition in one example.
Production Insight
Without types, a simple refactor like renaming a property can silently break dozens of call sites.
The TypeScript compiler will find those breaks instantly.
Rule: use static typing as a compile-time safety net — never rely on manual testing to catch type mismatches.
Key Takeaway
Static typing turns runtime errors into compile-time errors.
You catch bugs before they ever touch a user.
That's the fundamental reason TypeScript exists: shift error discovery left.
When to Add Types vs When to Leverage Inference
IfFunction returns a complex object
UseAdd explicit return type annotation to document the shape — it acts as a contract
IfLocal variable with obvious initial value
UseLet TypeScript infer the type — it reduces redundancy and keeps code clean
IfThird-party library without type definitions
UseCreate a minimal .d.ts declaration file — avoid using any or the library will be untyped

Interfaces 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.

productCatalogue.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// --- INTERFACE: describes the shape of a product in our store ---
interface Product {
  id: number;
  name: string;
  priceInCents: number;   // storing price as integer cents avoids floating-point bugs
  inStock: boolean;
  tags: string[];         // an array of strings
}

// Interfaces can be extended — great for specialised variants
interface DigitalProduct extends Product {
  downloadUrl: string;    // only digital products have this field
  fileSizeMb: number;
}

// --- TYPE ALIAS: perfect for unions and computed types ---
// A cart item is a Product plus a quantity — intersection type
type CartItem = Product & {
  quantity: number;
};

// An API response is either data or an error — union type
type ApiResponse<T> = 
  | { success: true; data: T }        // happy path
  | { success: false; error: string }; // error path

// --- Real usage: a function that processes a cart ---
function calculateCartTotal(cartItems: CartItem[]): number {
  return cartItems.reduce((total, item) => {
    // TypeScript knows 'item.priceInCents' is a number — no coercion needed
    return total + item.priceInCents * item.quantity;
  }, 0);
}

const shoppingCart: CartItem[] = [
  { id: 1, name: 'Mechanical Keyboard', priceInCents: 12999, inStock: true, tags: ['hardware', 'peripherals'], quantity: 1 },
  { id: 2, name: 'USB-C Cable',         priceInCents: 1499,  inStock: true, tags: ['accessories'],            quantity: 3 },
];

const totalCents = calculateCartTotal(shoppingCart);
console.log(`Total: $${(totalCents / 100).toFixed(2)}`); // Total: $167.96

// --- ApiResponse in action ---
function handleProductFetch(response: ApiResponse<Product>): void {
  if (response.success) {
    // TypeScript NARROWS the type here — it knows 'response.data' exists
    console.log(`Fetched: ${response.data.name}`);
  } else {
    // Inside this branch TypeScript knows 'response.error' exists
    console.error(`Fetch failed: ${response.error}`);
  }
}
Output
Total: $167.96
Interface vs Type — The Quick Rule:
Default to 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.
Production Insight
Conflicting type annotations can cause subtle bugs when code is refactored.
If you have a type intersection that silently overrides a property, TypeScript may not warn you until runtime.
Rule: choose interface for shared contracts — they give clearer error messages and support declaration merging.
Key Takeaway
Interface is for contracts, type is for combinations.
When in doubt, start with interface — you can always change to type later.
Know the difference so your code communicates intent clearly.
Interface or Type: Which One Should You Use?
IfObject shape that will be extended or implemented by classes
UseUse interface — it is designed for extension and gives better tooling support
IfUnion of two or more types (e.g., string | number)
UseUse type alias — interfaces cannot represent unions
IfYou need computed types using keyof or mapped types
UseUse type alias — interfaces have limited support for mapped types

Interface vs Type: Detailed Comparison Matrix

While both interface and type can define object shapes, they have distinct capabilities and limitations that affect how you model data. Choosing the wrong one can lead to verbose code or unexpected behavior when refactoring. Here's a side-by-side comparison of their key features:

FeatureInterfaceType Alias
Primitive alias❌ Cannot alias string, number✅ Can alias primitives, e.g., type ID = string
Union types❌ Cannot represent `AB`✅ `type Result = SuccessFailure`
Intersection typesCan use extends for composition✅ Using & operator
Declaration merging✅ Multiple declarations merge❌ Duplicate type redeclaration error
Computed/mapped types❌ Limited support✅ Full support for keyof, Record<K,T>, Pick<T,K>
Error messagesClearer – conflict shown at declarationSometimes cryptic – conflict shown at usage
Extending other typesextends keywordIntersection & (can lead to silent overrides)
Class implementationimplements InterfaceName✅ Also works, but less conventional
PerformanceSlightly faster for large types (cached)Slightly slower for deeply nested intersections

This table isn't just academic: choosing the wrong tool directly impacts compile-time error quality and maintainability. For production code where object shapes serve as shared contracts (e.g., API request/response models, database entities, component props), always start with interface. Reserve type for scenarios that demand unions, computed types, or primitive aliases. When you need both extension and union, consider splitting: define the base shape as an interface, then create union types from it.

interfaceVsType.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Example: When declaration merging matters (interface)
// Suppose you have multiple patches to a User type from different files:

// file1.ts
declare global {
  interface Window {
    appVersion: string;
  }
}

// file2.ts
declare global {
  interface Window {
    userLocale: string;
  }
}

// Both declarations merge — Window now has both properties
console.log(window.appVersion);  // OK
console.log(window.userLocale); // OK

// With type alias, this would be an error:
// type Window = { appVersion: string };
// type Window = { userLocale: string };  // ❌ Duplicate identifier 'Window'

// Example: Intersections vs extensions
interface Base {
  id: number;
}
interface Derived extends Base {
  name: string;
} // Easy to see inheritance

type BaseT = { id: number };
type DerivedT = BaseT & { name: string }; // Works, but chain can get messy

// Union type (type alias only)
type Status = 'active' | 'inactive' | 'pending';
Quick Decision Framework:
If you are describing a 'thing' in your domain (User, Product, Order) → use interface. If you need to combine or transform types (unions, intersections, computed) → use type. When in doubt, start with interface and switch to type only when you hit a limitation.
Production Insight
When you use type intersections for large object shapes, a property conflict may compile silently — the last type wins — and you won't discover the bug until runtime. With interface extends, TypeScript reports the conflict immediately at the declaration. This alone is a strong reason to prefer interface for core domain models.
Key Takeaway
Interface excels at definition and extension; type excels at combination and computation. Choose based on what your code needs to express, not which is newer.

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.

apiClient.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// A generic wrapper for fetch — works for ANY resource type
// 'T' is a placeholder that TypeScript fills in based on how you call this function
async function fetchResource<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      // Return a typed error response
      return { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
    }

    // TypeScript treats the parsed JSON as type T — we're asserting trust here
    const data = await response.json() as T;
    return { success: true, data };

  } catch (networkError) {
    return { success: false, error: 'Network request failed' };
  }
}

// --- Our domain types ---
interface BlogPost {
  id: number;
  title: string;
  body: string;
  authorId: number;
}

interface Comment {
  id: number;
  postId: number;
  text: string;
}

// --- Same function, different types — TypeScript tracks both ---
async function loadBlogData(): Promise<void> {
  // TypeScript infers 'result' as ApiResponse<BlogPost>
  const postResult = await fetchResource<BlogPost>(
    'https://jsonplaceholder.typicode.com/posts/1'
  );

  if (postResult.success) {
    // TypeScript KNOWS postResult.data is a BlogPost here
    // — so .title is valid, .nonExistentField would be a compile error
    console.log(`Post title: ${postResult.data.title}`);
  }

  // TypeScript infers 'commentResult' as ApiResponse<Comment>
  const commentResult = await fetchResource<Comment>(
    'https://jsonplaceholder.typicode.com/comments/1'
  );

  if (commentResult.success) {
    // TypeScript knows this is a Comment — .text is valid
    console.log(`First comment: ${commentResult.data.text.slice(0, 40)}...`);
  }
}

// --- A simpler generic utility to demonstrate the concept ---
// Takes any array, returns the first and last elements with full type safety
function getArrayBounds<T>(items: T[]): { first: T; last: T } | null {
  if (items.length === 0) return null;
  return {
    first: items[0],
    last: items[items.length - 1],
  };
}

const temperatures = [18.2, 22.7, 19.1, 25.3, 21.0];
const bounds = getArrayBounds(temperatures); // TypeScript infers T as number

if (bounds) {
  console.log(`Coldest recorded: ${bounds.first}°C`);
  console.log(`Most recent: ${bounds.last}°C`);
}

// TypeScript would flag this — you can't do .toFixed() on a string:
// const nameBounds = getArrayBounds(['Alice', 'Bob']);
// console.log(nameBounds?.first.toFixed(2)); // Error: Property 'toFixed' does not exist on type 'string'
Output
Post title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
First comment: id labore ex et quam laborum...
Coldest recorded: 18.2°C
Most recent: 21.0°C
Watch Out: `as T` is a Trust Assertion, Not a Guarantee
When you write 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.
Production Insight
Using any inside a generic function defeats the purpose — you lose all type safety.
The whole point of generics is to pass through the exact type without modification.
Rule: never use any in a generic constraint; prefer unknown and narrow the type explicitly.
Key Takeaway
Generics let you write flexible code that still catches type mismatches.
They are the only way to achieve reusability without falling back to any.
Master generics to build truly type-safe abstractions.
When to Use Generics vs When to Use Union Types
IfFunction should accept multiple types but preserve the relationship between input and output
UseUse generics — they preserve and propagate the exact type
IfYou only need a limited set of allowed types (e.g., string | number)
UseUse a union type — simpler and more explicit about allowed types
IfYou need constraints on the type (e.g., must have a .length property)
UseUse generics with constraints (extends) — union types cannot express constraints

Common Utility Types Cheat Sheet: Pick, Omit, Partial

TypeScript ships with several built-in utility types that save you from manually rewriting object shapes. The most frequently used in production code are Partial, Pick, and Omit. These are mapped types: they transform an existing type into a new type by adding or removing optionality or selecting specific keys. Understanding them is essential because they let you create derived types without duplicating interfaces, keeping your type definitions dry.

Partial<T> — makes every property in T optional. Use when you need to represent a partial update (e.g., PATCH request body) or a partially constructed object. Be careful: accessing a property that may now be undefined requires null checks.

Pick<T, K> — creates a type with only the specified keys K from T. Use when you want a subset of an interface, like a lightweight view of a user record that only includes name and email.

Omit<T, K> — creates a type with all keys from T except the specified keys K. Use when you want to exclude sensitive fields (e.g., password, ssn) from a response type, or when you need to remove a key before processing.

These utilities are the workhorses of real-world TypeScript projects. Combined with generics, they let you build type-safe data transformers, update functions, and API response filters with minimal boilerplate.

utilityTypes.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Base interface used for all examples
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: 'admin' | 'user';
}

// ──────────────────────────────────────────────
// 1. Partial<User> → all properties become optional
// ──────────────────────────────────────────────
function updateUser(userId: number, changes: Partial<User>): void {
  // 'changes' can have any subset of User properties
  console.log(`Updating user ${userId} with:`, changes);
}

updateUser(1, { name: 'Alice' });          // OK
updateUser(1, { email: 'a@b.com', role: 'admin' }); // OK
updateUser(1, {});                         // OK — empty object is a valid Partial

// ──────────────────────────────────────────────
// 2. Pick<User, 'name' | 'email'> → only those keys
// ──────────────────────────────────────────────
type UserContact = Pick<User, 'name' | 'email'>;
// Equivalent to: { name: string; email: string }

function sendWelcomeEmail(contact: UserContact): void {
  console.log(`Sending email to ${contact.name} at ${contact.email}`);
}

sendWelcomeEmail({ name: 'Bob', email: 'bob@example.com' }); // OK
// sendWelcomeEmail({ name: 'Bob' }); // ❌ Property 'email' is missing

// ──────────────────────────────────────────────
// 3. Omit<User, 'password'> → all keys except 'password'
// ──────────────────────────────────────────────
type PublicUser = Omit<User, 'password'>;
// Equivalent to: { id: number; name: string; email: string; role: 'admin' | 'user' }

function getUserProfile(user: PublicUser): void {
  console.log(`${user.name} (${user.email}) - Role: ${user.role}`);
  // TypeScript does NOT allow access to user.password
}

// ──────────────────────────────────────────────
// Combining with generics: a reusable partial update handler
// ──────────────────────────────────────────────
async function patchResource<T>(
  url: string,
  changes: Partial<T>
): Promise<void> {
  await fetch(url, {
    method: 'PATCH',
    body: JSON.stringify(changes),
    headers: { 'Content-Type': 'application/json' },
  });
}

// Usage
await patchResource<User>('/api/users/1', { email: 'new@example.com' });
// TypeScript ensures you only pass valid keys of User
Partial Can Bite You: Empty Objects Are Valid
Partial<T> accepts {}. If you iterate over keys of a Partial, you may end up with no operations. Always validate that at least one expected property was provided before performing an update. Consider using a union of partials with a discriminant or a custom type like AtLeastOne<T> if you need guarantees.
Production Insight
Using Pick and Omit to derive response types from domain models is a common pattern for preventing exposure of sensitive fields. However, nesting utilities (e.g., Partial<Pick<T, K>>) can make error messages harder to read. Keep utility chains short and document the resulting shape for maintainability.
Key Takeaway
Partial, Pick, and Omit are the three most used utility types in TypeScript. Master them to derive types from existing interfaces instead of writing redundant types.

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.

tsconfig.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
  "compilerOptions": {
    // What version of JavaScript to compile DOWN to
    // 'ES2020' supports async/await, optional chaining, etc.
    "target": "ES2020",

    // Module system — use 'NodeNext' for Node.js, 'ESNext' for bundlers
    "module": "NodeNext",
    "moduleResolution": "NodeNext",

    // Where compiled .js files go — keep them separate from your source
    "outDir": "./dist",

    // Where TypeScript looks for your source files
    "rootDir": "./src",

    // ✅ THE MOST IMPORTANT FLAG — enables all strict checks at once
    // This includes: strictNullChecks, noImplicitAny, strictFunctionTypes, etc.
    "strict": true,

    // Errors if you declare a variable and never use it
    // Keeps your codebase clean during refactors
    "noUnusedLocals": true,

    // Errors if a function parameter is declared but never used
    "noUnusedParameters": true,

    // Errors if a code path in a function doesn't return a value
    // Catches missing return statements in switch/if branches
    "noImplicitReturns": true,

    // Allows importing .json files as typed modules
    "resolveJsonModule": true,

    // Enables source maps so debuggers show your .ts files, not compiled .js
    "sourceMap": true,

    // Allows TypeScript to understand ES modules alongside CommonJS
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Output
// This is a config file — running `tsc` with these settings produces:
// ✅ Compiled output in ./dist
// ✅ Source maps for debugging
// ✅ Errors for any implicit 'any', unused variables, or missing null checks
Pro Tip: Migrate Gradually with `strict: false` + Individual Flags
Converting a large JS project to TS overnight is a recipe for burnout. Instead, start with 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.
Production Insight
A project with strict: false and noImplicitAny: false is essentially JavaScript with extra syntax — the type system is turned off.
Real-world outages often trace back to null values slipping through because strictNullChecks was disabled.
Rule: never run TypeScript in production without strict: true. It's not optional.
Key Takeaway
TypeScript's strict mode is not optional for production.
Without strict: true you are getting maybe 30% of the type safety you think you are.
Set it. Fix the errors. Your future self will thank you.
Which Strict Flag to Enable and When
IfYou are starting a new TypeScript project
UseSet strict: true from the beginning — it's easier to follow strict rules than to add them later
IfYou are migrating an existing JavaScript codebase
UseEnable noImplicitAny first, then strictNullChecks, then strictFunctionTypes, one per iteration
IfYou keep encountering null-related crashes in production
UseEnable strictNullChecks immediately and fix all resulting errors before the next deployment

Essential tsconfig.json Flags: strict, noImplicitAny, and More

While the previous section explained the strict mode philosophy, this is your quick reference for the most important compiler flags you need to know for production TypeScript. Every flag here has a direct impact on the safety and maintainability of your codebase.

FlagEffectWhy It Matters
strict: trueEnables all strict type-checking options.Without it, TypeScript loses ~70% of its safety. This is a one-line kill switch for the entire strict family.
strictNullChecksnull and undefined become distinct types.The #1 cause of production bugs in TypeScript projects that disable it. Every variable that can be null must be handled.
noImplicitAnyErrors when TypeScript cannot infer a type and silently falls back to any.Prevents accidental escape hatches. Without it, a variable typed as any disables type checking for that entire chain.
strictFunctionTypesChecks function parameter types more rigorously (contravariance).Catches unsafe subclassing where a callback expects a more specific type than provided.
noUnusedLocalsErrors on unused local variables.Keeps dead code out of your codebase, especially important during refactors.
noUnusedParametersErrors on unused function parameters.Prevents misleading function signatures. Use prefix underscore (_) to intentionally ignore.
noImplicitReturnsErrors when a function has code paths that don't return a value.Prevents accidental undefined returns when you intended to always return a value.
exactOptionalPropertyTypesEnsures optional properties don't allow undefined implicitly.Prevents a common pitfall where { name?: string } could be set to undefined explicitly.

For new projects, set strict: true and never look back. For migrations, start with noImplicitAny and strictNullChecks. Once those compile clean, turn on strictFunctionTypes. The table above serves as a checklist you can paste into your pull request template as a config review reminder.

minimal-strict-tsconfig.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,                          // All flags below are now on
    // Individual flags (can be toggled off during migration)
    "strictNullChecks": true,                // Already enabled by strict
    "noImplicitAny": true,                   // Already enabled by strict
    "strictFunctionTypes": true,             // Already enabled by strict
    "noUnusedLocals": true,                  // Not part of strict, add manually
    "noUnusedParameters": true,              // Not part of strict, add manually
    "noImplicitReturns": true,               // Not part of strict, add manually
    "exactOptionalPropertyTypes": false,     // Advanced, enable after migration
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
Incremental Adoption Recipe
If you're migrating a 50k-line codebase: Week 1 – enable noImplicitAny and fix all errors (may need any in some places, but note where). Week 2 – enable strictNullChecks and start using optional chaining. Week 3 – enable strictFunctionTypes and refactor any callbacks that fail. Week 4 – flip strict: true and handle remaining edge cases.
Production Insight
The most common mistake in TypeScript production deployments is disabling strict mode because 'it's too hard to fix all the errors'. Every error you suppress with strict: false is a potential production crash. Invest the time upfront — it's cheaper than a post-mortem at 2 AM.
Key Takeaway
Use the table above as your TypeScript config checklist. strict: true is the baseline; add noUnusedLocals, noUnusedParameters, and noImplicitReturns for an additional layer of tidiness.

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.

typeNarrowing.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// --- Using typeof and truthiness checks ---
function printLength(value: string | null): void {
  if (value) {
    // Inside this block, 'value' is automatically narrowed to 'string'
    // TypeScript knows null and undefined are falsy
    console.log(value.length);
  } else {
    console.log('No value provided');
  }
}

// --- Using instanceof for class instances ---
class Order {
  constructor(public id: number, public total: number) {}
}

class Refund {
  constructor(public orderId: number, public amount: number, public reason: string) {}
}

type Transaction = Order | Refund;

function processTransaction(tx: Transaction): void {
  if (tx instanceof Order) {
    // TypeScript knows tx is Order here
    console.log(`Order #${tx.id}: $${(tx.total / 100).toFixed(2)}`);
  } else {
    // TypeScript knows tx is Refund here
    console.log(`Refund for order #${tx.orderId}: ${tx.reason}`);
  }
}

// --- User-defined type guard ---
interface ApiError {
  errorCode: number;
  message: string;
}

interface ApiSuccess<T> {
  data: T;
}

// A custom type guard function
function isApiError<T>(response: ApiError | ApiSuccess<T>): response is ApiError {
  return 'errorCode' in response;
}

function handleApiResponse<T>(response: ApiError | ApiSuccess<T>): void {
  if (isApiError(response)) {
    // Inside this block, TypeScript knows response is ApiError
    // and we can safely access errorCode and message
    console.error(`Error ${response.errorCode}: ${response.message}`);
    // If we tried to access response.data, TypeScript would error
  } else {
    // Response is ApiSuccess<T>
    console.log('Data received:', response.data);
  }
}
Output
Order #1234: $89.99
Refund for order #5678: Customer request
Error 404: Not found
Pro Tip: Use Discriminated Unions for API Responses
Instead of using a type guard with 'in', define a discriminant property like 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.
Production Insight
Over-reliance on custom type guards can lead to runtime errors if the guard logic is wrong.
The guard function itself is not checked by TypeScript — it can incorrectly narrow types and mislead the compiler.
Rule: always test your custom type guards with unit tests, and prefer discriminated unions when possible.
Key Takeaway
Type narrowing is how you safely work with union types.
The compiler trusts your checks — make sure they are correct.
Use discriminated unions for complex data; they are easier to narrow and less error-prone.
Which Narrowing Technique Should You Use?
IfYou need to check a primitive type (string, number, boolean)
UseUse typeof type guard — it's built-in and reliable
IfYou need to check an object's shape based on a property
UseUse in operator or a discriminated union with a literal property check
IfYou need to check a class instance
UseUse instanceof — it checks the prototype chain and is safe
IfYou have a complex condition that needs to be reused
UseWrite a custom user-defined type guard with the 'value is Type' return type

Why Your Codebase Is Still Full of `any` — And How to Kill It for Good

You know the drill. Some junior—or let's be honest, probably you at 2 AM—slaps as any on a response from an API and calls it a day. That any doesn't just turn off type checking. It erases the entire contract between your code and the data it processes. When that API shape changes next sprint, you don't get a compile-time error. You get a Cannot read properties of undefined in production at 3 PM on a Friday.

The fix isn't just 'use strict mode.' You need a zero-tolerance policy for implicit any in function returns. TypeScript's noImplicitReturns flag catches functions that forget to return a value. But the real weapon is strictNullChecks combined with noUncheckedIndexedAccess. That combo forces you to handle every undefined branch explicitly. No more optional chaining as a crutch—you actually check the thing exists before you touch it.

Here's the pattern that senior teams enforce: every function has an explicit return type. If you see a function without one, that's a code review fail. Period. When you type the return, the compiler catches mistakes you didn't even know you made. And when you're consuming external data, use a runtime validator like Zod or io-ts to bridge the gap between the wild west of JSON and your typed world. TypeScript doesn't run at runtime—your validators do.

ApiResponseGuard.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — javascript tutorial

// The problem: implicit return types hide broken contracts
function fetchUser(id: string) {
  const raw = fetch(`/users/${id}`).then(r => r.json());
  return raw; // type is Promise<any>
}

// The fix: explicit return type + runtime validation
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer'])
});

async function fetchUserSafe(id: string): Promise<z.infer<typeof UserSchema>> {
  const raw = await fetch(`/users/${id}`).then(r => r.json());
  const parsed = UserSchema.safeParse(raw);
  
  if (!parsed.success) {
    throw new Error(`User fetch failed validation: ${parsed.error.message}`);
  }
  
  return parsed.data; // fully typed, null-free
}
Output
If you call fetchUserSafe with id='bad-uuid', it will throw: 'User fetch failed validation: Expected string at id'
Production Trap:
Using as casts on API responses is the number one source of runtime crashes in typed TypeScript codebases. The compiler trusts you. Don't make it a liar.
Key Takeaway
Every function must have an explicit return type; every external data source must have a runtime schema validator.

The Union Type Trick That Prevents Invalid States at Compile Time

Most devs think union types are just for 'string or number.' That's like using a scalpel to open a can of beans. The real power of union types is modelling mutually exclusive states so the compiler eliminates entire classes of bugs before they compile.

Consider a form that can be in three states: idle, submitting, or error. The naive approach is three booleans: isIdle, isSubmitting, isError. But booleans are a liar's data type—they let you represent states that don't make sense, like isIdle=true and isSubmitting=true simultaneously. That's a bug that lives in the gap between what you intend and what the type system allows.

Instead, model the state as a discriminated union. Each variant carries only the data relevant to that state. When you're in 'submitting', there's no error message field. When you're in 'error', there's no result data. The compiler enforces this—not your code review, not your tests, not your prayers. This pattern is called 'making illegal states unrepresentable.' It's not buzzword fluff—it's the difference between a codebase you trust and one you're scared to deploy on a Friday.

Apply this to API responses, UI states, navigation stacks—anywhere you'd reach for an enum or a set of flags. Your type definitions become executable documentation that the compiler enforces.

FormStateMachine.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// io.thecodeforge — javascript tutorial

// The wrong way: three booleans that lie to you
interface BadFormState {
  isIdle: boolean;
  isSubmitting: boolean;
  isError: boolean;
  errorMessage?: string;
  result?: PaymentData;
}
// Bug: { isIdle: true, isSubmitting: true, isError: false } — compiles fine

// The right way: discriminated union of valid states
type PaymentFormState =
  | { status: 'idle' }
  | { status: 'submitting'; paymentId: string }
  | { status: 'succeeded'; transaction: TransactionReceipt }
  | { status: 'failed'; errorCode: number; errorMessage: string };

function renderForm(state: PaymentFormState) {
  switch (state.status) {
    case 'idle':
      return '<button>Pay Now</button>';
    case 'submitting':
      return `<spinner /> Processing payment ${state.paymentId}`;
    case 'succeeded':
      return `<Receipt ${state.transaction.amount} />`; // safe access
    case 'failed':
      return `<ErrorBanner code=${state.errorCode} />`; // only error fields here
  }
}
Output
TypeScript will error if you try to access state.transaction when status is 'failed'. The compiler prevents the mismatch.
Senior Shortcut:
When you see a React component with three boolean props, that's a code smell. Replace with a union type of literal status strings. Your future self debugging at 2 AM will buy you a beer.
Key Takeaway
If your types can represent impossible states, your code will eventually reach them. Use discriminated unions to make illegal states unrepresentable.

Classes Are Not Your Dump Truck — When OOP Actually Pays Off in TypeScript

TypeScript classes aren't Java. You don't need a class for every noun in your domain model. The real value of classes in TypeScript is encapsulation of state with guaranteed invariants, not inheritance pyramids that collapse in month two.

Use classes when you have mutable state that must stay consistent — a bank account that can't go negative, a WebSocket connection manager that enforces a max reconnection count. For data-only structures, interfaces or type aliases are cheaper and easier to reason about. The private, protected, and readonly modifiers exist so you can enforce constraints from the constructor onward, not so you can write getters for every field.

The implements keyword is your friend. Code against interfaces, not concrete classes. That way you can swap implementations without touching callers. And please, stop putting business logic in constructors. Build objects with factory functions or static methods, keep constructors simple assignments.

BankAccount.tsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// io.thecodeforge — javascript tutorial

interface Account {
  readonly id: string;
  balance: number;
  deposit(amount: number): void;
}

class CheckingAccount implements Account {
  readonly id: string;
  private _balance: number = 0;

  constructor(id: string, initial: number) {
    if (initial < 0) throw new Error('Negative initial balance');
    this.id = id;
    this._balance = initial;
  }

  get balance(): number {
    return this._balance;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Deposit must be positive');
    this._balance += amount;
  }
}

const acct = new CheckingAccount('acct-1', 100);
acct.deposit(50);
console.log(acct.balance);
Output
150
Production Trap:
Never expose a mutable array or object via a getter — clone it or return a readonly version. Callers will mutate your internals and you'll spend a Friday debugging ghost state.
Key Takeaway
Class = encapsulated mutable state with invariants. Data = interface. Don't mix them.

Testing TypeScript Means Testing The Types, Not Just The Values

Your test suite should fail when types break before a single runtime assertion runs. Unit tests prove your logic works for given inputs. Type tests prove your generics, unions, and overloads actually constrain callers correctly.

Use @ts-expect-error in tests — place it on lines you expect to be type errors. If the line compiles without error, the test fails. This catches API contract violations during CI. For example, if you change a function signature, the type test that passed a string where a number was expected will now fail to compile, and your type-level test pinpoints exactly which callers broke.

For runtime testing, pick Vitest over Jest. It's faster, uses the same describe/it/expect API you already know, and handles TypeScript and ESM natively without babel acrobatics. Mock minimally — if your module is hard to test without mocking, your module is coupled wrong, not your test framework.

Production rule: a PR that changes types without adding a type test is incomplete. Period.

typeTest.tsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — javascript tutorial

import { expect, test } from 'vitest';

type ExtractString<T> = T extends string ? T : never;

// Runtime test: logic works
function identity<T>(x: T): T {
  return x;
}

test('identity returns same value', () => {
  expect(identity(42)).toBe(42);
});

// Type test: contract is correct
// @ts-expect-error — number should be blocked
type Result = ExtractString<42>;

// @ts-expect-error — number should be blocked
type Result2 = ExtractString<true>;

// This should compile clean
type Result3 = ExtractString<'hello'>;
Output
✓ identity returns same value
Senior Shortcut:
Write type tests right next to runtime tests in the same file. One PR, one review, full coverage. Tools like tsd and expect-type give you expectTypeOf for even cleaner assertions.
Key Takeaway
A type test that compiles is an assertion that passes. Use @ts-expect-error to assert type errors — your CI should catch contract breaks.

Classes Aren’t a Silver Bullet — When OOP Actually Pays Off in TypeScript

TypeScript classes exist for when you need encapsulated state that changes over time — a bank account, a game character, a WebSocket connection. If your data is just a blob of fields, use an interface or type alias. Classes add runtime cost, constructor boilerplate, and prototypal inheritance complexity. Reach for a class when you need methods that mutate internal state, when you want to enforce invariants via private fields, or when you rely on instanceof type guards. For pure data transfer objects, interfaces with factory functions are lighter and compose better. The trap: using classes everywhere because you came from Java or C#. That kills TypeScript’s structural typing advantages. Reserve classes for objects with behavior that must stay coupled to mutable state. Everything else stays plain objects with functions.

OopVsData.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — javascript tutorial

// Bad: class for static data
class User {
  constructor(public name: string) {}
}

// Good: interface for data, function for behavior
interface User {
  name: string;
}
const greet = (u: User) => `Hello ${u.name}`;

// Good: class for mutable state with invariants
class Wallet {
  private balance = 0;
  deposit(amount: number) {
    if (amount <= 0) throw new Error('Invalid');
    this.balance += amount;
  }
}
Output
No runtime output — compile-time typing.
Wallet.balance is truly private.
User class is useless wrapper.
Production Trap:
Never use class getters/setters for simple field access — they break destructuring and add 3x overhead.
Key Takeaway
Classes are for mutable state with invariants; interfaces for everything else.

TypeScript Interview Questions That Test Real Understanding

Interviewers love asking about keyof, conditional types, and the infer keyword — because those reveal whether you truly grasp TypeScript’s type system or just skate by with any. Expect: "How does typeof differ in TypeScript vs JavaScript?" (JS: runtime string tag; TS: compile-time type query). "What does extends do in generics vs classes?" (Generics: constraint; Classes: inheritance). "Write a type that extracts the return type from a function." (Use infer R in a conditional type). The real test: explain why Pick<T, K> is safer than Omit<T, K> when K is a union (Pick fails at compile-time if K has an invalid member; Omit silently returns T). Another classic: "How do discriminated unions prevent invalid states?" Show a union of objects with a kind literal field, then switch on it — TypeScript narrows the payload per case. Master these patterns, and you’ll pass any senior-level TypeScript screen.

InterviewPatterns.tsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — javascript tutorial

// Discriminated union — golden interview answer
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rect'; w: number; h: number };

const area = (s: Shape) =>
  s.kind === 'circle'
    ? Math.PI * s.radius ** 2  // s is narrowed
    : s.w * s.h;

// Conditional type with infer
type ReturnOf<T> =
  T extends (...args: any[]) => infer R ? R : never;

// Usage
type Fn = (x: string) => number;
type R = ReturnOf<Fn>;  // number
Output
No runtime output.
Compiler infers R = number.
Discriminated union gives 100% type safety.
Production Trap:
Never hand-write ReturnType — it breaks on overloaded functions. Use Awaited<ReturnType<...>> for async.
Key Takeaway
Discriminated unions + conditional types with infer are the core of advanced TypeScript interview questions.
● Production incidentPOST-MORTEMseverity: high

The Missing Null Check That Crashed Payment Processing

Symptom
Occasional orders completed but payment reconciliation showed zero amounts. Users reported successful checks but merchants never received funds.
Assumption
The migration was safe because the codebase was 'clean'. The team assumed that because they had no type errors left, the code was correct.
Root cause
The 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.
Fix
Enabled 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.
Key lesson
  • 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 any and missing null handling. Automate this with ESLint rules.
Production debug guideSymptom → Action pairs for the errors that waste the most engineer hours3 entries
Symptom · 01
TypeScript error: 'Property X does not exist on type Y'
Fix
Check if the property is optional or if the interface misses it. Run 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.
Symptom · 02
Any variable implicitly has type 'any'
Fix
Fix by adding explicit type annotation or enabling noImplicitAny in tsconfig. Use generics if the type is unknown but should be constrained. Avoid any - prefer unknown and type narrowing.
Symptom · 03
Type 'string | undefined' is not assignable to type 'string'
Fix
This is strictNullChecks telling you that a value may be undefined. Use optional chaining (?.), nullish coalescing (??), or explicit checks (if (value !== undefined)) to narrow the type before assignment.
★ Quick Debug Cheat Sheet: TypeScript Type ErrorsFive common type errors and the commands to diagnose and fix them fast
Type 'X' is missing the following properties from type 'Y': ...
Immediate action
Check if you forgot to include a required property in an object literal
Commands
tsc --noEmit --pretty
In your editor, hover over the object literal to see expected type
Fix now
Add the missing property or use a type assertion if the object is not yet complete
'X' is used before being assigned+
Immediate action
Check variable declaration - TypeScript cannot determine it is always assigned
Commands
tsc --noEmit --pretty 2>&1 | grep -i 'used before'
Look for code paths where the variable might not be assigned (e.g., if-else branches)
Fix now
Initialize the variable, or use a definite assignment assertion (!) only if you are sure
Type 'string[]' is not assignable to type 'number[]'+
Immediate action
Array element type mismatch - check the generic parameter of the array
Commands
tsc --noEmit --pretty | head -20
Check the function's return type or the variable's declared type
Fix now
Correct the type or use as if you are certain the runtime value matches (but prefer proper types)
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type+
Immediate action
You are trying to access an object property with a dynamic key, but TS can't verify the key exists
Commands
Check if the object has a string index signature: `[key: string]: unknown`
Alternatively, use a `Map` or a properly typed record
Fix now
Add a type guard or use keyof to constrain the index type
TypeScript vs JavaScript: Key Differences at a Glance
Feature / AspectJavaScriptTypeScript
Type systemDynamic — checked at runtimeStatic — checked at compile time
Null safetyNo enforcement — null bugs are runtime surprisesEnforced with strictNullChecks — null must be handled explicitly
IDE supportBasic autocomplete from inferenceRich IntelliSense, rename refactors, go-to-definition across files
Runtime behaviourRuns directly in browser/Node.jsCompiled to JS first — same runtime, types are erased
Onboarding large codebasesRequires reading source or docs to understand data shapesTypes ARE the documentation — shapes are visible in signatures
Learning curveShallow — start writing immediatelySteeper initially — pays off significantly on projects >5k lines
Community/toolingUniversalNear-universal — adopted by Angular, Vue 3, Next.js, NestJS, etc.
Error discoveryAt runtime (or in user's browser)At write-time in your editor, before any code runs

Key takeaways

1
TypeScript's types are erased at runtime
they exist only to protect you at development time, which means zero performance cost and full compatibility with any JS environment.
2
The `strict
true compiler flag is not optional for serious projects — without it (especially without strictNullChecks`) you're getting maybe 30% of TypeScript's actual safety benefits.
3
Generics let you write one reusable function or class that maintains full type safety across different input types
they're the alternative to any when you need flexibility without sacrificing correctness.
4
An 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 patterns
knowing which to reach for makes your code more readable to other TypeScript developers.
5
Type narrowing is essential for working with union types safely. Use built-in type guards (typeof, instanceof, in) or custom type guard functions, but always test custom guards to ensure correctness.

Common mistakes to avoid

4 patterns
×

Using `any` as an escape hatch everywhere

Symptom
TypeScript stops flagging errors, you get false confidence your code is 'typed', then runtime bugs creep back in.
Fix
Replace 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

Symptom
You merge two types with & expecting them to behave like an interface extension, but conflict errors are cryptic and hard to debug.
Fix
Interfaces surface merge conflicts clearly at the declaration site with messages like 'Types of property X are incompatible'. Type intersections surface the conflict only at usage, making errors harder to trace. When combining domain object shapes, use 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

Symptom
You write if (myValue instanceof MyInterface) expecting it to work, but you get a runtime error because interfaces don't exist in compiled JavaScript.
Fix
Interfaces are compile-time constructs only. For runtime type checking use type guards (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

Symptom
Null-related crashes happen in production despite TypeScript compilation succeeding. The type system did not prevent the most common source of runtime errors.
Fix
Enable 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between `interface` and `type` in TypeScript, and...
Q02SENIOR
Explain what TypeScript generics are and give a practical example of why...
Q03SENIOR
What does `strictNullChecks` do, why is it disabled by default, and what...
Q01 of 03SENIOR

What is the difference between `interface` and `type` in TypeScript, and when would you choose one over the other?

ANSWER
Both define object shapes, but 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Do I need to rewrite my entire JavaScript project to use TypeScript?
02
Does TypeScript make JavaScript run faster?
03
What's the difference between a TypeScript compile error and a runtime error?
04
Can I use TypeScript with React?
05
How do I handle third-party JavaScript libraries that don't have TypeScript types?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's TypeScript. Mark it forged?

15 min read · try the examples if you haven't

Previous
MERN Stack: MongoDB, Express, React, and Node.js
1 / 15 · TypeScript
Next
TypeScript Types and Interfaces