D
Dev SOPKnowledge Base
Search
← All topics

TypeScript Advanced Types: Generics, Utility Types, and Type Guards

Production TypeScript patterns: conditional types, mapped types, template literal types, branded types, discriminated unions, and full-stack type safety with Zod.

typescriptgenericstype-safetyutility-typesfull-stack
Agent trigger phrases: TypeScript generics · conditional types · mapped types · branded types · discriminated unions · Zod type inference · TypeScript utility types

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");
}