Overview
Advanced TypeScript for building robust, type-safe applications. These patterns appear repeatedly in Next.js, Convex, and API integrations.
Strict Configuration (Non-Negotiable)
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true
}
}
noUncheckedIndexedAccess is the most impactful non-default flag — catches array[0] being undefined at compile time.
Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type IsArray<T> = T extends unknown[] ? true : false;
// Distribute over union members
type Flatten<T> = T extends Array<infer U> ? U : T;
// Flatten<string[]> → string
// Flatten<(string|number)[]> → string | number
Mapped Types
// Make all properties optional and nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Pick only function properties
type Methods<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
// Rename keys with template literals
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
Branded Types (Opaque Primitives)
Prevent mixing of semantically different strings/numbers:
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };
function createUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId) { /* ... */ }
const userId = createUserId("user_123");
const postId = "post_456" as PostId;
getUser(userId); // OK
getUser(postId); // Type error — cannot assign PostId to UserId
Discriminated Unions (Error Handling)
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function parseUser(raw: unknown): Result<User> {
const parsed = UserSchema.safeParse(raw);
if (!parsed.success) return { success: false, error: parsed.error };
return { success: true, data: parsed.data };
}
// At call site — exhaustive narrowing
const result = parseUser(rawData);
if (result.success) {
console.log(result.data.name); // data is typed as User
} else {
console.error(result.error); // error is typed as ZodError
}
Zod — Runtime Validation + Type Inference
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.string().datetime(),
});
type User = z.infer<typeof UserSchema>; // TypeScript type from schema
// Validate at API boundary
export async function POST(req: Request) {
const body = await req.json();
const parsed = UserSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 });
}
// parsed.data is fully typed as User
}
Full-Stack Type Safety Pattern
Share types between frontend and backend:
// types/api.ts — shared between client and server
export interface ApiResponse<T> {
data: T;
meta: { page: number; total: number };
}
export type UserListResponse = ApiResponse<User[]>;
// Server (route.ts)
export async function GET(): Promise<Response> {
const users = await db.query.users.findMany();
const payload: UserListResponse = { data: users, meta: { page: 1, total: users.length } };
return Response.json(payload);
}
// Client
const res = await fetch("/api/users");
const json: UserListResponse = await res.json(); // Typed at consumption
Template Literal Types
type EventName = `on${Capitalize<string>}`;
type CSSProperty = `${string}-${string}`;
type Route = `/api/${string}`;
// Strongly typed event bus
type EventMap = {
"user:created": { userId: string };
"user:deleted": { userId: string };
"post:published": { postId: string; authorId: string };
};
function emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) { /* */ }
emit("user:created", { userId: "123" }); // OK
emit("user:created", { postId: "123" }); // Error — wrong payload shape
Type Guard Functions
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value
);
}
// Assertion function (throws on failure)
function assertUser(value: unknown): asserts value is User {
if (!isUser(value)) throw new TypeError("Expected User");
}