Loading...
Loading...
Provides TypeScript patterns for type-first development, making illegal states unrepresentable, exhaustive handling, and runtime validation. Must use when reading or writing TypeScript/JavaScript files.
npx skill4agent add duvesalo/app typescript-best-practices.tsx.jsx@reactreact-best-practices// Good: only valid combinations possible
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Bad: allows invalid combinations like { loading: true, error: Error }
type RequestState<T> = {
loading: boolean;
data?: T;
error?: Error;
};type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
// Compiler prevents passing OrderId where UserId expected
function getUser(id: UserId): Promise<User> { /* ... */ }
function createUserId(id: string): UserId {
return id as UserId;
}const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'
// Array and type stay in sync automatically
function isValidRole(role: string): role is Role {
return ROLES.includes(role as Role);
}// Creation: some fields required
type CreateUser = {
email: string;
name: string;
};
// Update: all fields optional
type UpdateUser = Partial<CreateUser>;
// Database row: all fields present
type User = CreateUser & {
id: UserId;
createdAt: Date;
};foo.test.tsfoo.tsconstletreadonlyReadonly<T>array.map/filter/reduceforstrictswitchneverawaitexport function buildWidget(widgetType: string): never {
throw new Error(`buildWidget not implemented for type: ${widgetType}`);
}type Status = "active" | "inactive";
export function processStatus(status: Status): string {
switch (status) {
case "active":
return "processing";
case "inactive":
return "skipped";
default: {
const _exhaustive: never = status;
throw new Error(`unhandled status: ${_exhaustive}`);
}
}
}export async function fetchWidget(id: string): Promise<Widget> {
const response = await fetch(`/api/widgets/${id}`);
if (!response.ok) {
throw new Error(`fetch widget ${id} failed: ${response.status}`);
}
return response.json();
}import debug from "debug";
const log = debug("myapp:widgets");
export function createWidget(name: string): Widget {
log("creating widget: %s", name);
const widget = { id: crypto.randomUUID(), name };
log("created widget: %s", widget.id);
return widget;
}z.infer<>safeParseparse.extend().pick().omit().merge().transform().refine()// ✅ Zod 4: top-level validators
z.email()
z.url()
z.uuid()
// ❌ Zod 3 legacy (still works but prefer the above)
z.string().email()
z.string().url()
z.string().uuid()errormessage// ✅ Zod 4
z.email({ error: "Invalid email format" })
z.string().min(1, { error: "Required" })
// ❌ Zod 3 legacy
z.string().email({ message: "Invalid email format" })import { z } from "zod";
const UserSchema = z.object({
id: z.uuid(),
email: z.email(),
name: z.string().min(1),
createdAt: z.string().transform((s) => new Date(s)),
});
type User = z.infer<typeof UserSchema>;import { z, SafeParseReturnType } from "zod";
export function parseUserInput(raw: unknown): SafeParseReturnType<unknown, User> {
return UserSchema.safeParse(raw);
}
// Caller handles both success and error:
const result = parseUserInput(formData);
if (!result.success) {
setErrors(result.error.flatten().fieldErrors);
return;
}
await submitUser(result.data);export async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`fetch user ${id} failed: ${response.status}`);
}
const data = await response.json();
return UserSchema.parse(data); // throws if API contract violated
}const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = CreateUserSchema.partial();
const UserWithPostsSchema = UserSchema.extend({
posts: z.array(PostSchema),
});process.envimport { z } from "zod";
const ConfigSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.url(),
API_KEY: z.string().min(1),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
export const config = ConfigSchema.parse(process.env);import { config } from "./config";
const server = app.listen(config.PORT);
const db = connect(config.DATABASE_URL);Opaque<T, Token>& { __brand }PartialDeep<T>ReadonlyDeep<T>LiteralUnion<Literals, Fallback>SetRequired<T, K>SetOptional<T, K>Simplify<T>import type { Opaque, PartialDeep, SetRequired } from 'type-fest';
// Branded type (cleaner than manual approach)
type UserId = Opaque<string, 'UserId'>;
// Deep partial for patch operations
type UserPatch = PartialDeep<User>;
// Make specific fields required
type UserWithEmail = SetRequired<Partial<User>, 'email'>;