

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
overany
. Narrow with checks. - Enable
strict
intsconfig
. KeepnoImplicitAny
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 orunknown
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.