When TypeScript Builds but Production Fails: Understanding Type Safety

Krupa SawantKrupa Sawant
5 min read

If you’ve ever written a TypeScript app that runs flawlessly on your local machine but refuses to deploy on Vercel (or any production server), you’re not alone. One of the most common causes of these failures is weak or missing type definitions. Locally, a relaxed configuration can allow unsafe patterns to slip through. In production, stricter settings — often enforced through tsconfig.json or ESLint — block the build entirely.

The biggest culprit is the any type. Declaring a variable as any tells the compiler, “skip the checks, I’ll handle it.” This makes experimentation easy during development but defeats the whole purpose of TypeScript’s safety guarantees. The moment your project moves to an environment that enforces rules like @typescript-eslint/no-explicit-any, those unchecked variables trigger errors that stop the build.

The rest of this article walks through why these errors happen, how to avoid them, and how tools like Supabase can help you maintain strong, automatically generated types that stay in sync with your database.

Why Local Builds Pass and CI Fails

Your local dev server is usually forgiving. tsconfig.json may have loose settings, lint isn’t always blocking, and you’re clicking “run” on a machine that doesn’t mind any. Vercel and most CI pipelines, on the other hand, run strict: true, ESLint, and fail the build if a type cannot be trusted.

// ❌ nothing is checked
let product: any = { id: 123, name: "Sneaker" };
product.price.toFixed(); // may explode at runtime

// ✅ compiler guards you
type Product = { id: number; name: string; price: number };
let safeProduct: Product = { id: 123, name: "Sneaker", price: 79.99 };

The first snippet “works” locally but CI will halt on any when rules like @typescript-eslint/no-explicit-any are enforced.

Build Types Into Your Workflow

The best way to avoid build-stopping errors is to declare exactly what your data looks like. Instead of falling back on any, create small, meaningful types that describe the expected structure:

1. Define Explicit Shapes

export type Product = {
  id: string;
  name: string;
  price: number;
};

When the compiler knows price must be a number, it can stop a bug before runtime. You no longer rely on manual checks; the type system enforces them for you.


2. Generate Types from Supabase

Keeping application types synchronized with your database by hand is tedious and error-prone. Supabase solves this by generating TypeScript definitions directly from your schema:

npx supabase gen types typescript \
  --project-id "your-project-id" \
  --schema public > src/database.types.ts

This creates a single file containing all table and column definitions, ensuring your code matches the database at all times. Any schema change immediately reflects in the generated types.


3. Why Supabase Joins Are a Game-Changer

A standout feature of Supabase is that you can query related tables directly in a single select statement. Instead of writing separate queries for products and product_variants, you write:

const { data } = await supabase
  .from("products")
  .select(`
    id,
    name,
    product_variants(
      id,
      size,
      stock
    )
  `);

The API returns nested JSON:

[
  {
    "id": "123",
    "name": "Sneaker",
    "product_variants": [
      { "id": "v1", "size": "M", "stock": 8 },
      { "id": "v2", "size": "L", "stock": 5 }
    ]
  }
]

Why it’s great:

  • One query – no hand-rolled SQL

  • Types included – auto-complete for product_variants

  • Less boilerplate – React can consume the result immediately


4. Map Raw DB Types to UI-Friendly Types

Database rows often include extra fields or nested structures you don’t need in the interface. Transform them early so your components consume exactly what they expect. This step also lets you define your own types, rename fields, and flatten nested shapes without changing the database schema.

// Raw type straight from Supabase
type SupabaseProduct = {
  id: string;
  name: string;
  product_variants: { id: string; size: "S" | "M" | "L" }[];
};

// Clean UI type
type ProductWithVariants = {
  id: string;
  name: string;
  variants: { id: string; size: "S" | "M" | "L" }[];
};

const fetchProducts = async (): Promise<ProductWithVariants[]> => {
  const { data, error } = await supabase
    .from("products")
    .select("id, name, product_variants(id, size)");

  if (error || !data) return [];

  return (data as SupabaseProduct[]).map((p) => ({
    id: p.id,
    name: p.name,
    variants: p.product_variants,
  }));
};

Think of it like:

Warehouse Manifest (raw) → Storefront Catalog (clean)

5. Extra Safety Habits

// Prefer 'unknown' over 'any'
function parseJSONSafe(str: string): unknown {
  return JSON.parse(str);
}

// Literal unions for statuses
const STATUSES = ["pending", "paid", "shipped"] as const;
type Status = typeof STATUSES[number];

// Handle null early
type Profile = { id: string; full_name: string | null };
const displayName = (p: Profile) => p.full_name ?? "Anonymous";

Visualizing the Flow

┌────────────┐   supabase gen types   ┌─────────────┐    .map() transform   ┌────────────┐
│  Database  │ ─────────────────────▶ │  TS Types   │ ────────────────────▶ │  React UI │
└────────────┘                        └─────────────┘                       └────────────┘

Define schema → Generate types → Fetch & join → Clean map → Render safely


Quick Pre-Deploy Checklist

  • strict: true in tsconfig.json

  • Regenerate types whenever schema changes

  • Avoid any (use unknown if you must)

  • Map DB types to clean app types

  • Handle null & undefined

  • Use literal enums for fixed statuses


Takeaway

Strong typing is not optional in production. Make your shapes explicit, generate types straight from your database, and transform raw data before it reaches your UI. With strict mode on, TypeScript will catch errors before you deploy instead of after. Supabase’s type generator keeps your schema and code in sync so your builds pass locally and on Vercel.

0
Subscribe to my newsletter

Read articles from Krupa Sawant directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Krupa Sawant
Krupa Sawant