Why we use TypeScript
Femi Omojuwa
•  

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.