Why we use TypeScript
Femi Omojuwa
Femi Omojuwa
•   Aug 10, 2025

Why we use TypeScript

We ship production software. TypeScript helps us do it with fewer bugs, safer refactors, and clear contracts between components, APIs, and teams. It feels like a seatbelt that does not get in the way.

What TypeScript gives you

  • Fewer bugs: problems are caught before runtime.
  • Refactors you can trust: change code with confidence.
  • Self-documenting code: types explain intent.
  • Clear API contracts: front end and back end agree.
  • Great editor support: autocomplete and inline help everywhere.

A simple example

JavaScript allows this mistake. TypeScript stops it early.

// TS catches this at compile time
const add = (a: number, b: number) => a + b;
// add(1, "2"); // Error: Argument of type 'string' is not assignable to 'number'

Types you will use every day

Use aliases and interfaces for clarity.

type UserId = string;

interface User {
  id: UserId;
  name: string;
  email?: string; // optional
}

const user: User = { id: "u_1", name: "Ada" };

Safer data shapes with unions and narrowing

Model success and error clearly. No more guessing.

type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };

function getName(result: ApiResult<User>) {
  if (result.ok) {
    return result.data.name; // fully typed
  }
  return "unknown";
}

Generics for reusable helpers

Write it once, keep types intact.

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}

const brief = pick({ id: "1", name: "Ada", admin: false }, ["id", "name"]);

Next.js + TypeScript patterns

Keep server data typed from fetch to component.

// types.ts
export type Post = { id: string; title: string; publishedAt: string };

// data.ts
export async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://api.example.com/posts", { cache: "no-store" });
  if (!res.ok) throw new Error("Failed to load posts");
  const json = (await res.json()) as Post[]; // typed layer
  return json;
}

// app/(portfolio)/blog/page.tsx
export default async function Page() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Runtime safety with Zod, types for free

Validate at runtime, infer types at compile time.

import { z } from "zod";

const PostSchema = z.object({
  id: z.string(),
  title: z.string(),
  publishedAt: z.string().datetime(),
});
type Post = z.infer<typeof PostSchema>;

async function parsePosts(input: unknown): Promise<Post[]> {
  return z.array(PostSchema).parseAsync(input);
}

Typed events and DOM

No more guessing event shapes.

type FormValues = { email: string };

const Signup = () => {
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const fd = new FormData(e.currentTarget);
    const values: FormValues = { email: String(fd.get("email") ?? "") };
    // ...
  };
  return <form onSubmit={onSubmit}>{/* ... */}</form>;
};

Edge and server safety

We deploy on the Edge. Types help avoid window in server code and keep boundaries clean.

const isBrowser = typeof window !== "undefined";
// Guard client-only logic
if (isBrowser) {
  // safe to use window or document here
}

API contracts that scale

Share types across client and server for stable contracts.

export type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string };

export interface CreateProjectInput {
  name: string;
  budgetUSD?: number;
}

export type Project = { id: string; name: string; budgetUSD?: number };

async function createProject(input: CreateProjectInput): Promise<ApiResponse<Project>> {
  // validate, insert, return typed result
  return { ok: true, data: { id: "p1", name: input.name, budgetUSD: input.budgetUSD } };
}

Our guardrails

  • Prefer unknown over any. Narrow with checks.
  • Enable strict in tsconfig. Keep noImplicitAny on.
  • Use as const and discriminated unions for stable states.
  • Keep utilities small and typed. Test logic that matters.
function parseJSON<T>(s: string): T | null {
  try {
    return JSON.parse(s) as T;
  } catch {
    return null;
  }
}

When plain JavaScript is fine

Quick spikes, tiny demos, or throwaway scripts. For product code and shared libraries, we default to TypeScript.

Why this helps teams

  • New teammates onboard faster with types as maps.
  • Reviews focus on behavior, not shape-checking.
  • Refactors land faster because the compiler guides changes.
  • Bugs shift left, which saves time and trust.

Get started fast

  • Add TypeScript, enable strict mode, rename files to .ts or .tsx.
  • Type boundaries first: API inputs, outputs, and component props.
  • Replace any with real types or unknown plus guards.
  • Validate inputs at runtime with Zod, infer types once.

TypeScript lets us move fast without breaking things we care about. That is why it is our default.