Level up your TypeScript with these 10 essential tips covering generics, utility types, strict patterns, and real-world patterns from production Next.js applications.
any — Use unknown InsteadThe any type defeats the purpose of TypeScript. In our Developer Portfolio & SaaS Platform, we enforce strict mode with zero any types across 60+ models.
// ❌ Bad — bypasses all type checking
function parseData(data: any) {
return data.name.toUpperCase();
}
// ✅ Good — forces runtime type checking
function parseData(data: unknown)
Instead of optional fields and boolean flags, use discriminated unions:
// ❌ Bad — unclear which fields are available when
type ApiResponse = {
loading: boolean;
data?: User;
error?: string;
};
// ✅ Good — each state is explicit
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case "loading":
return <Spinner />;
case "success":
return <UserCard user={response.data} />; // `data` is guaranteed
case "error":
return <ErrorMessage message={response.error} />; // `error` is guaranteed
}
}
TypeScript types vanish at runtime. Use Zod schemas that generate types AND validate data:
import { z } from "zod";
const UserSchema = z.object({
email: z.string().email("Invalid email"),
name: z.string().min(2, "Name too short").max(100),
role: z.enum(["SUPER_ADMIN", "CLIENT", "CUSTOMER", "AFFILIATE"]),
age: z.number().int().positive().optional(),
});
// Auto-generate TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Runtime validation in Server Actions
export async function createUser(formData: FormData) {
const result = UserSchema.safeParse({
email: formData.get("email"),
name: formData.get("name"),
role: formData.get("role"),
});
if (!result.success) {
return { error: result.error.errors[0].message };
}
// result.data is fully typed as User
await prisma.user.create({ data: result.data });
}
This is the exact pattern we use across our 25+ server action files.
satisfies for Type-Safe Object LiteralsThe satisfies operator (TypeScript 4.9+) validates types without widening:
type Theme = {
colors: Record<string, string>;
spacing: Record<string, number>;
};
// ✅ Type-checked AND preserves literal types
const theme = {
colors: {
primary: "#6366f1",
secondary: "#8b5cf6",
accent: "#f59e0b",
},
spacing: {
sm: 8,
md: 16,
lg: 24,
},
} satisfies Theme;
// theme.colors.primary is type string (not just string)
// TypeScript will error if you add unknown keys
// ❌ Too loose — accepts anything
function getProperty<T>(obj: T, key: string) {
return obj[key]; // Error: no index signature
}
// ✅ Constrained — only valid keys allowed
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Hardik", role: "SUPER_ADMIN", age: 28 };
getProperty(user, "name"); // Returns string
getProperty(user, "age"); // Returns number
getProperty(user, "email"); // ❌ Compile error — 'email' not in keyof User
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
// ✅ Only valid combinations allowed
const endpoint: ApiEndpoint = "GET /api/users"; // OK
const bad: ApiEndpoint = "PATCH /api/users"; // ❌ Error
// Real-world: dynamic event names
type EventName = `on${Capitalize<"click" | "hover" | "focus">}`;
// Result: "onClick" | "onHover" | "onFocus"
const Assertions for Immutable Data// Without const assertion — types are widened
const routes = ["/home", "/about", "/blog"]; // string[]
// With const assertion — preserves literal types
const routes = ["/home", "/about", "/blog"] as const;
// readonly ["/home", "/about", "/blog"]
type Route = (typeof routes)[number];
// "/home" | "/about" | "/blog"
// Perfect for configuration objects
const CONFIG = {
API_URL: "https://api.example.com",
MAX_RETRIES: 3,
ROLES: ["SUPER_ADMIN", "CLIENT", "CUSTOMER"] as const,
} as const;
Prevent mixing up IDs and strings that look the same:
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = "cuid123" as UserId;
const orderId = "cuid456" as OrderId;
getUser(userId); // ✅ OK
getUser(orderId); // ❌ Compile error — can't pass OrderId as UserId
type AllRoles = "SUPER_ADMIN" | "CLIENT" | "CUSTOMER" | "AFFILIATE";
// Only admin roles
type AdminRoles = Extract<AllRoles, "SUPER_ADMIN">;
// Non-admin roles
type UserRoles = Exclude<AllRoles, "SUPER_ADMIN">;
// "CLIENT" | "CUSTOMER" | "AFFILIATE"
// Extract return types from async functions
type ActionReturn = Awaited<ReturnType<typeof loginAction>>;
// { error?: string; success?: string }
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) return { ok: false, error: "User not found" };
return { ok: true, value: user };
} catch (e) {
return { ok: false, error: "Database error" };
}
}
// Usage — forces handling both cases
const result = await fetchUser("cuid123");
if (result.ok) {
console.log(result.value.name); // TypeScript knows `value` exists
} else {
console.error(result.error); // TypeScript knows `error` exists
}
Here's the tsconfig.json we use in our production projects:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}
These patterns aren't just academic — they're used daily in our Developer Portfolio & SaaS Platform across 60+ Prisma models and 25+ server action files. Strict TypeScript catches bugs before your users do.
Related reads:
Follow Hardik Kanajariya on LinkedIn for daily TypeScript tips.
Building type-safe applications? Check out our Developer Portfolio & SaaS Platform — 100% TypeScript strict mode, zero any types, starting at $299.
Get the latest articles, tutorials, and updates delivered straight to your inbox. No spam, unsubscribe at any time.