Expert TypeScript development for frontend applications. Covers type safety, patterns, generics, utility types, and best practices. Use for any TypeScript code.
Install
npx skillscat add hwatkins/my-skills/frontend-typescript Install via the SkillsCat registry.
SKILL.md
TypeScript Development
You are an expert in TypeScript with deep knowledge of type safety, modern patterns, and frontend development.
Core Principles
- Leverage the type system to catch errors at compile time
- Prefer strict mode (
"strict": truein tsconfig) - Use types to make impossible states impossible
- Avoid
any— useunknownwhen the type is truly unknown - Write self-documenting code with descriptive types
Type Fundamentals
Prefer Interfaces for Objects, Types for Unions/Intersections
// ✅ Good: Interface for object shapes
interface User {
id: string;
email: string;
name: string;
role: UserRole;
}
// ✅ Good: Type for unions and computed types
type UserRole = "admin" | "editor" | "viewer";
type UserWithPosts = User & { posts: Post[] };
// ✅ Good: Type for function signatures
type EventHandler<T> = (event: T) => void;Use Discriminated Unions for State
// ✅ Good: Discriminated union — impossible states are impossible
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case "idle":
return null;
case "loading":
return <Spinner />;
case "success":
return <Data data={state.data} />;
case "error":
return <ErrorMessage error={state.error} />;
}
}
// ❌ Bad: Separate booleans — allows impossible states
interface BadState {
isLoading: boolean;
isError: boolean;
data?: User;
error?: Error;
// Can isLoading AND isError both be true? Unclear.
}Generics
- Use generics for reusable, type-safe abstractions
- Constrain generics with
extendswhen needed - Use meaningful names (
Tfor type,Kfor key,Vfor value)
// ✅ Good: Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// ✅ Good: Generic with default
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// ✅ Good: Generic component props
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}Utility Types
Use built-in utility types instead of reinventing them:
// Partial — all properties optional
type UpdateUser = Partial<User>;
// Pick — select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Omit — exclude properties
type CreateUser = Omit<User, "id" | "createdAt">;
// Record — typed key-value map
type RolePermissions = Record<UserRole, Permission[]>;
// Required — make all properties required
type CompleteUser = Required<User>;
// Extract / Exclude — filter union types
type ActiveStatus = Extract<Status, "active" | "pending">;Strict Null Handling
- Enable
strictNullChecks(included instrict: true) - Use optional chaining (
?.) and nullish coalescing (??) - Narrow types with type guards
// ✅ Good: Type narrowing
function processUser(user: User | null) {
if (!user) {
return;
}
// TypeScript knows user is User here
console.log(user.name);
}
// ✅ Good: Custom type guard
function isAdmin(user: User): user is AdminUser {
return user.role === "admin";
}
// ✅ Good: Nullish coalescing
const displayName = user.name ?? "Anonymous";
const port = config.port ?? 3000;
// ❌ Bad: Non-null assertion without checking
const name = user!.name; // DangerousAsync Patterns
// ✅ Good: Typed async functions
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return response.json() as Promise<User>;
}
// ✅ Good: Error handling with Result type
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safelyFetchUser(id: string): Promise<Result<User>> {
try {
const user = await fetchUser(id);
return { ok: true, value: user };
} catch (error) {
return { ok: false, error: error as Error };
}
}Enums vs Const Objects
Prefer as const objects over enums for better tree-shaking and type inference:
// ✅ Preferred: const object
const Status = {
Active: "active",
Inactive: "inactive",
Suspended: "suspended",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// Also acceptable: string enum
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}Module Organization
- One export per concept, co-locate related types
- Use barrel exports (
index.ts) sparingly — they can hurt tree-shaking - Export types separately with
export typefor better erasure
// ✅ Good: Co-located types and implementation
// user.ts
export interface User {
id: string;
email: string;
}
export type CreateUserInput = Omit<User, "id">;
export function createUser(input: CreateUserInput): User {
return { id: crypto.randomUUID(), ...input };
}Zod for Runtime Validation
Use Zod to bridge compile-time types with runtime validation:
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "editor", "viewer"]),
});
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}Common Mistakes
// ❌ Don't use `any`
function process(data: any) { ... }
// ✅ Use `unknown` and narrow
function process(data: unknown) {
if (typeof data === "string") { ... }
}
// ❌ Don't use type assertions carelessly
const user = data as User; // No runtime check!
// ✅ Validate at boundaries
const user = UserSchema.parse(data);
// ❌ Don't use `object` type
function process(obj: object) { ... }
// ✅ Use specific types or Record
function process(obj: Record<string, unknown>) { ... }
// ❌ Don't ignore Promise rejections
fetchUser(id).then(setUser);
// ✅ Handle errors
fetchUser(id).then(setUser).catch(handleError);
// Or use async/await with try/catch