Reference Prompts

Prompt Engineering for TypeScript/React

Not general prompt theory — specific prompts for specific situations you face every day writing TypeScript and React. A recipe book, not a textbook.


Part 1: Why TypeScript/React Is Different

Prompting for TypeScript and React is different from prompting for general code because you're working with a type system and a component model that constrain what "correct" means. A Python function that works is done. A TypeScript function that works but uses any everywhere is not done. A React component that works but re-renders 40 times per keystroke is not done.

AI doesn't know your quality bar unless you tell it. These are the constraints that turn mediocre AI output into production-quality TypeScript/React code:

The TypeScript/React Prompt Formula

Types + Patterns + Constraints + Task. Paste the relevant types, name the patterns you want, list what to avoid, then describe what to build. This formula produces code that matches your project on the first generation — not the third.


Part 2: Type-First Prompting

The single most effective technique for TypeScript prompting: define your types first, then ask AI to implement against them. Types act as a contract — they constrain the output to something that compiles.

Define Types Before Implementation

You

Here are my types:

interface Task {
  id: string;
  title: string;
  status: 'todo' | 'in-progress' | 'review' | 'done';
  priority: 'low' | 'medium' | 'high' | 'urgent';
  assigneeId: string | null;
  projectId: string;
  position: number;
  createdAt: string;
  completedAt: string | null;
}

interface CreateTaskInput {
  title: string;
  status?: Task['status'];
  priority?: Task['priority'];
  assigneeId?: string;
  projectId: string;
}

type TaskUpdate = Partial<Omit<Task, 'id' | 'createdAt'>>;

Implement a createTask function that takes CreateTaskInput, generates an ID and timestamp, and returns a Task. Default status is 'todo', default priority is 'medium'.

AI now generates code that uses the exact types you defined. No guessing about field names, no inventing extra properties, no mismatched types. The output compiles on the first try because the contract was established upfront.

Compare this to the vague version: "Write a function that creates a task with a title, status, priority, and project ID." That prompt produces code with whatever field names AI invents — and they probably won't match your existing types.

Using Types to Infer Implementation

You

Here's my API response type and my Zustand store type:

// API response
interface ApiResponse<T> {
  data: T;
  meta?: { total: number; page: number; pageSize: number };
}

// Store
interface TaskStore {
  tasks: Task[];
  isLoading: boolean;
  error: string | null;
  pagination: { total: number; page: number; pageSize: number };
  fetchTasks: (projectId: string, page?: number) => Promise<void>;
  createTask: (input: CreateTaskInput) => Promise<Task>;
  updateTask: (id: string, update: TaskUpdate) => Promise<void>;
  deleteTask: (id: string) => Promise<void>;
}

Implement the Zustand store. The API base URL is /api. Handle loading and error states. Use immer middleware.

The store type itself tells AI exactly what to implement — every action, every state field, every return type. AI fills in the body of each function to match the signatures you defined. You're programming AI with TypeScript.

Getting AI to Infer Types From Usage

When you have code but no types (common when working with JavaScript codebases being migrated to TypeScript):

You

Here's a JavaScript module with no type annotations:

[paste the module]

And here's how it's used across the codebase:

[paste: grep -n "userService\." src/ -r --include="*.js"]

Infer TypeScript interfaces for all function parameters and return types based on actual usage. Be specific — use string literals and union types where the usage implies specific values.

The instruction "use string literals and union types where the usage implies specific values" prevents AI from typing everything as string. If every call passes 'admin' or 'user', the type should be 'admin' | 'user', not string.

Pro Tip: Paste Types in Every Prompt

Even if AI "should" remember the types from earlier in the conversation, paste them again. Context drift is real — by message 15, AI may have subtly changed a field name or type. Re-pasting your types costs 5 seconds and prevents 10 minutes of debugging mismatched types.


Part 3: Component Prompts

React components have patterns that AI needs explicit guidance on. Without constraints, AI generates components that work but don't follow modern best practices.

The Base Component Prompt

You

Create a TaskCard component.

Props:

interface TaskCardProps {
  task: Task;
  onEdit: (id: string) => void;
  onDelete: (id: string) => void;
  isDragging?: boolean;
}

Requirements:

- Functional component, named export
- Show title, priority badge (color-coded), assignee name if assigned, due date if set
- Priority colors: low=gray, medium=blue, high=amber, urgent=red
- Edit button and delete button with confirm dialog
- isDragging adds opacity-50 and shadow-lg classes
- Use Tailwind for all styling
- No inline styles, no CSS modules

Every component prompt should include: the Props interface (typed), visual requirements, interaction behavior, styling approach, and anti-patterns to avoid. This produces a production-ready component on the first generation.

Custom Hook Prompts

You

Create a useDebounce hook.

// Desired signature:
function useDebounce<T>(value: T, delay: number): T

Requirements:

- Generic type parameter for any value type
- Clean up timeout on unmount and when value changes
- Return the debounced value
- Include JSDoc with usage example

For hooks, always include the desired function signature. This is the strongest constraint you can give AI — it defines the inputs, outputs, and generic parameters. AI fills in the implementation to match.

Composition Patterns

You

I have a Modal component that accepts children. Create a ConfirmDialog that uses it.

Here's the Modal component:

[paste Modal.tsx]

interface ConfirmDialogProps {
  title: string;
  message: string;
  confirmLabel?: string;  // default: "Confirm"
  cancelLabel?: string;   // default: "Cancel"
  variant?: 'danger' | 'warning' | 'info';  // affects confirm button color
  isOpen: boolean;
  onConfirm: () => void;
  onCancel: () => void;
}

Compose ConfirmDialog using the existing Modal. Don't duplicate Modal's functionality.

The key instruction is "Don't duplicate Modal's functionality." Without it, AI often generates a standalone dialog that reimplements the backdrop, positioning, and close behavior — ignoring the Modal you already have.

Preventing Common React Anti-Patterns

Add these constraints to your prompts when you see AI falling into them:


Part 4: State & Data Flow

State management and data fetching are where AI prompts need the most precision. These patterns affect the entire application — a wrong approach in a store propagates everywhere.

Zustand Store Prompt

You

Create a Zustand store for project management.

interface ProjectStore {
  projects: Project[];
  activeProjectId: string | null;
  isLoading: boolean;
  error: string | null;

  // Actions
  fetchProjects: () => Promise<void>;
  createProject: (name: string) => Promise<Project>;
  archiveProject: (id: string) => Promise<void>;
  setActiveProject: (id: string) => void;
}

Implementation rules:

- Use immer middleware for immutable updates
- API calls use this pattern: set loading → fetch → set data → clear loading. On error: set error message, clear loading.
- fetchProjects filters out archived projects
- createProject optimistically adds to list, reverts on API failure
- Auth token comes from useAuthStore.getState().token
- API base URL: /api

The critical details: specifying the loading/error pattern, the optimistic update behavior, and how to get the auth token. Without these, AI will implement a working store that handles none of these correctly.

Data Fetching With React Query

You

Create a custom hook for fetching and mutating tasks using TanStack Query (React Query).

// Query keys
const taskKeys = {
  all: ['tasks'] as const,
  byProject: (projectId: string) => [...taskKeys.all, projectId] as const,
  detail: (id: string) => [...taskKeys.all, 'detail', id] as const,
};

// Desired hooks:
// useTasks(projectId) — returns { tasks, isLoading, error }
// useTask(id) — returns { task, isLoading, error }
// useCreateTask() — returns mutation with optimistic update
// useUpdateTask() — returns mutation with optimistic update
// useDeleteTask() — returns mutation with optimistic removal

Rules:

- Mutations invalidate relevant queries on success
- Optimistic updates for create/update/delete
- Roll back optimistic updates on error
- Type everything — no any
- Stale time: 30 seconds for lists, 60 seconds for details

This prompt is dense, but each line prevents a specific mistake. The query key factory prevents cache invalidation bugs. The stale times prevent over-fetching. The optimistic update requirement prevents the jarring "flash of loading state" on every mutation.

Form State With React Hook Form

You

Create a task creation form using React Hook Form + Zod.

const createTaskSchema = z.object({
  title: z.string().min(1, 'Title is required').max(200),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
  assigneeId: z.string().uuid().nullable().default(null),
  dueDate: z.string().date().nullable().default(null),
});

type CreateTaskForm = z.infer<typeof createTaskSchema>;

Requirements:

- Use zodResolver for validation
- Show inline error messages below each field
- Disable submit button while submitting
- Reset form after successful submission
- onSubmit prop receives validated data
- Priority uses radio buttons, not a select
- Tailwind styling

Pasting the Zod schema is the key. It defines every field, every validation rule, every default value, and every type — and the form AI generates will match exactly.


Part 5: API & Backend Prompts

TypeScript on the backend follows the same type-first principle, but with additional concerns: validation, error handling, and database queries.

Express Route With Validation

You

Create an Express route for creating tasks.

// Shared types (used by both frontend and backend)
interface CreateTaskInput {
  title: string;
  status?: 'todo' | 'in-progress' | 'review' | 'done';
  priority?: 'low' | 'medium' | 'high' | 'urgent';
  assigneeId?: string;
  projectId: string;
}

// Zod schema for validation
const createTaskSchema = z.object({
  title: z.string().min(1).max(200),
  status: z.enum(['todo', 'in-progress', 'review', 'done']).default('todo'),
  priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
  assigneeId: z.string().uuid().optional(),
  projectId: z.string().uuid(),
});

Route requirements:

- POST /api/tasks
- Validate body with Zod schema, return 400 with specific field errors
- Check that projectId belongs to the authenticated user (userId from req.user)
- Use taskRepo.create() for database insertion
- Return 201 with { data: task }
- Wrap everything in try/catch, return 500 with error code on failure

Repository Pattern

You

Create a task repository using better-sqlite3.

interface TaskRepo {
  findByProject(projectId: string): Task[];
  findById(id: string): Task | null;
  create(input: CreateTaskInput & { id: string; createdAt: string }): Task;
  update(id: string, changes: TaskUpdate): Task | null;
  delete(id: string): boolean;
  move(id: string, status: Task['status'], position: number): Task | null;
}

Rules:

- All queries parameterized (no string interpolation)
- SELECT specifies columns (no SELECT *)
- create returns the full created row
- update returns null if the row doesn't exist
- move updates both status and position, sets completedAt when moved to 'done'
- Use db.prepare() for all queries

Specifying "no string interpolation" and "no SELECT *" explicitly prevents the two most common security and data exposure issues in AI-generated database code.

Error Handling Chain

You

Create a typed error handling system for the API.

class AppError extends Error {
  constructor(
    public code: string,      // e.g. "TASK_CREATE_TITLE_REQUIRED"
    public statusCode: number, // e.g. 400
    public details?: Record<string, string>  // field-level errors
  ) {
    super(code);
    this.name = 'AppError';
  }
}

// Express error handler middleware signature:
// (err: Error, req: Request, res: Response, next: NextFunction) => void

Create:

1. The AppError class (above)
2. Error factory functions: notFound(resource), validation(details), unauthorized(), forbidden()
3. Express error handler middleware that formats AppError vs unknown errors
4. An asyncHandler wrapper that catches promise rejections and forwards to the error handler

This prompt builds a complete error handling pipeline from the types outward. The factory functions prevent inconsistent error formats across routes. The asyncHandler wrapper prevents the "unhandled promise rejection" issue that plagues Express apps.


Part 6: The Prompt Library

Copy-paste-ready prompts for common TypeScript/React tasks. Customize the bracketed sections for your project.

New Component

Create a [ComponentName] component.

Props:
[paste your Props interface]

Requirements:
- Functional component, named export
- [describe visual layout and elements]
- [describe interactions and behavior]
- [describe conditional rendering]
- Use Tailwind for styling
- No any types
- Include JSDoc on the component

Custom Hook

Create a custom hook: [hookName]

Signature:
[paste desired function signature with generics]

Behavior:
- [describe what the hook manages]
- [describe side effects and cleanup]
- [describe return values]

Constraints:
- Clean up on unmount
- Handle race conditions if async
- Type all parameters and return values
- Include JSDoc with usage example

Zustand Store

Create a Zustand store for [domain].

Store interface:
[paste your store interface with all state and actions]

Implementation rules:
- Use immer middleware
- Loading pattern: set loading → fetch → set data → clear loading
- Error pattern: set error message, clear loading
- Auth token from: [how to get it]
- API base URL: [url]
- [any optimistic update requirements]
- [any specific caching or deduplication needs]

API Route

Create an Express route: [METHOD] [path]

Input validation:
[paste Zod schema]

Authorization:
- [who can access this route]
- [what ownership checks are needed]

Implementation:
- Use [repoName] for database access
- Return format: { data: [type] } on success
- Error format: { error: string, code: string }
- [specific business logic requirements]

Error handling:
- 400 for validation failures with field-level errors
- 401/403 for auth failures
- 404 for missing resources
- 500 with error code for unexpected failures

Test Suite

Write tests for [module/function name].

Source:
[paste the implementation]

Test framework: Vitest
Test structure:
- describe block per public function
- Happy path for each function
- Error/edge cases: [list specific cases]
- Mock these dependencies: [list]

Patterns:
- Use factories for test data, not raw objects
- Assert specific values, not just truthiness
- Test error messages, not just that errors are thrown
- [any project-specific test conventions]

Type Extraction

Here's a JavaScript module with no types:
[paste the module]

Here's how it's used:
[paste grep results of usage across codebase]

Generate TypeScript interfaces for:
- All function parameters
- All return types
- Any internal data structures

Rules:
- Use string literals and union types where usage implies specific values
- Use readonly for arrays that aren't mutated
- Use optional (?) for parameters that aren't always provided
- No any — use unknown if the type truly can't be determined

Refactor for Performance

This React component re-renders too often. Optimize it.

Current component:
[paste component]

Current re-render triggers:
[describe what causes unnecessary re-renders]

Allowed optimizations:
- useMemo for expensive computations
- useCallback for event handlers passed as props
- React.memo for child components
- Move state down to the component that needs it

Not allowed:
- Don't change the component's API (props)
- Don't change the visual output
- Don't add new dependencies

Migration: JavaScript to TypeScript

Convert this JavaScript file to TypeScript.

Source:
[paste .js file]

Usage context:
[paste imports/calls from other files]

Rules:
- Infer types from usage, not from generic patterns
- Use strict types — no any
- Use string literals and unions where values are constrained
- Add readonly where arrays aren't mutated
- Keep the exact same runtime behavior
- Keep the same exports (named/default)
- Add JSDoc where the types alone don't explain intent

TypeScript/React Prompts — Summary

Back to Home