Intermediate18 min1 prerequisite

Recognize and work with common TypeScript patterns found in AI-generated React and Next.js applications.

TypeScript Patterns in AI Code

AI tools generate consistent TypeScript patterns. Learn to recognize and modify these patterns confidently.

React Component Patterns

Functional Components

Terminal
// Basic component with props
interface GreetingProps {
  name: string
  className?: string
}

export function Greeting({ name, className }: GreetingProps) {
  return <h1 className={className}>Hello, {name}!</h1>
}

// Alternative: inline type
export function Greeting({ name, className }: {
  name: string
  className?: string
}) {
  return <h1 className={className}>Hello, {name}!</h1>
}

Props with Children

Terminal
interface CardProps {
  title: string
  children: React.ReactNode
  className?: string
}

export function Card({ title, children, className }: CardProps) {
  return (
    <div className={className}>
      <h2>{title}</h2>
      {children}
    </div>
  )
}

Extending HTML Elements

Terminal
// Button that accepts all native button props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "default" | "outline" | "ghost"
  size?: "sm" | "md" | "lg"
}

export function Button({
  variant = "default",
  size = "md",
  className,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

forwardRef Pattern

Terminal
import { forwardRef } from "react"

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, className, ...props }, ref) => {
    return (
      <div>
        {label && <label>{label}</label>}
        <input ref={ref} className={className} {...props} />
      </div>
    )
  }
)
Input.displayName = "Input"

Hook Patterns

useState with Types

Terminal
// Primitive types - inferred
const [count, setCount] = useState(0)           // number
const [name, setName] = useState("")            // string
const [active, setActive] = useState(false)    // boolean

// Complex types - explicit
const [user, setUser] = useState<User | null>(null)
const [users, setUsers] = useState<User[]>([])
const [data, setData] = useState<ApiResponse>()  // ApiResponse | undefined

// Object state
interface FormState {
  name: string
  email: string
  errors: Record<string, string>
}

const [form, setForm] = useState<FormState>({
  name: "",
  email: "",
  errors: {}
})

useRef with Types

Terminal
// DOM element refs
const inputRef = useRef<HTMLInputElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const divRef = useRef<HTMLDivElement>(null)

// Mutable value refs
const countRef = useRef<number>(0)
const timerRef = useRef<NodeJS.Timeout | null>(null)
const previousValue = useRef<string>()

useEffect Dependencies

Terminal
// Effect with typed dependencies
useEffect(() => {
  const fetchData = async () => {
    const response = await fetch(`/api/users/${userId}`)
    const data: User = await response.json()
    setUser(data)
  }
  fetchData()
}, [userId])  // TypeScript ensures userId is correct type

Custom Hook Return Types

Terminal
// Tuple return (like useState)
function useToggle(initial: boolean): [boolean, () => void] {
  const [value, setValue] = useState(initial)
  const toggle = useCallback(() => setValue(v => !v), [])
  return [value, toggle]
}

// Object return
interface UseUserReturn {
  user: User | null
  loading: boolean
  error: Error | null
  refetch: () => void
}

function useUser(id: string): UseUserReturn {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  const refetch = useCallback(() => {
    // fetch logic
  }, [id])

  return { user, loading, error, refetch }
}

Event Handler Patterns

Common Event Types

Terminal
// Mouse events
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault()
}

const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
  // ...
}

// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  const formData = new FormData(e.currentTarget)
}

// Input events
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const value = e.target.value
  setName(value)
}

const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const selected = e.target.value
}

// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === "Enter") {
    handleSubmit()
  }
}

Event Handler Props

Terminal
interface FormProps {
  onSubmit: (data: FormData) => void | Promise<void>
  onChange?: (field: string, value: string) => void
  onError?: (error: Error) => void
}

function Form({ onSubmit, onChange, onError }: FormProps) {
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    try {
      await onSubmit(new FormData(e.currentTarget as HTMLFormElement))
    } catch (err) {
      onError?.(err as Error)
    }
  }

  return <form onSubmit={handleSubmit}>...</form>
}

Next.js Patterns

Page Props

Terminal
// App Router page
interface PageProps {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default function Page({ params, searchParams }: PageProps) {
  const { slug } = params
  const page = searchParams.page
  return <div>Slug: {slug}</div>
}

// With Promise (Next.js 15+)
interface PageProps {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export default async function Page({ params, searchParams }: PageProps) {
  const { slug } = await params
  const { page } = await searchParams
  return <div>Slug: {slug}</div>
}

Layout Props

Terminal
interface LayoutProps {
  children: React.ReactNode
  params: { locale: string }
}

export default function Layout({ children, params }: LayoutProps) {
  return (
    <html lang={params.locale}>
      <body>{children}</body>
    </html>
  )
}

Server Actions

Terminal
"use server"

interface ActionResult {
  success: boolean
  message?: string
  error?: string
}

export async function createUser(
  prevState: ActionResult,
  formData: FormData
): Promise<ActionResult> {
  const name = formData.get("name") as string
  const email = formData.get("email") as string

  try {
    // Create user logic
    return { success: true, message: "User created" }
  } catch (error) {
    return { success: false, error: "Failed to create user" }
  }
}

API Routes

Terminal
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server"

interface User {
  id: string
  name: string
  email: string
}

export async function GET(request: NextRequest) {
  const users: User[] = await getUsers()
  return NextResponse.json(users)
}

export async function POST(request: NextRequest) {
  const body: Partial<User> = await request.json()

  if (!body.name || !body.email) {
    return NextResponse.json(
      { error: "Missing required fields" },
      { status: 400 }
    )
  }

  const user = await createUser(body as Omit<User, "id">)
  return NextResponse.json(user, { status: 201 })
}

Zod Schema Patterns

Form Validation

Terminal
import { z } from "zod"

// Define schema
const userSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  age: z.number().min(18, "Must be 18 or older").optional(),
})

// Infer TypeScript type from schema
type User = z.infer<typeof userSchema>
// { name: string; email: string; age?: number }

// Validate data
function validateUser(data: unknown): User {
  return userSchema.parse(data)  // Throws if invalid
}

// Safe validation
function safeValidateUser(data: unknown) {
  const result = userSchema.safeParse(data)
  if (result.success) {
    return result.data  // User type
  } else {
    return result.error  // ZodError
  }
}

With React Hook Form

Terminal
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

const formSchema = z.object({
  username: z.string().min(2),
  email: z.string().email(),
})

type FormValues = z.infer<typeof formSchema>

function ProfileForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
    },
  })

  function onSubmit(values: FormValues) {
    // values is typed as { username: string; email: string }
    console.log(values)
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* form fields */}
    </form>
  )
}

API Response Patterns

Typed Fetch

Terminal
interface User {
  id: string
  name: string
  email: string
}

interface ApiError {
  message: string
  code: string
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`)

  if (!response.ok) {
    const error: ApiError = await response.json()
    throw new Error(error.message)
  }

  const user: User = await response.json()
  return user
}

Generic API Response

Terminal
interface ApiResponse<T> {
  data: T | null
  error: string | null
  status: "idle" | "loading" | "success" | "error"
}

// Usage with any type
const userResponse: ApiResponse<User> = {
  data: { id: "1", name: "Alice", email: "alice@test.com" },
  error: null,
  status: "success"
}

const usersResponse: ApiResponse<User[]> = {
  data: [],
  error: null,
  status: "success"
}

Discriminated Unions

Terminal
// Result type pattern
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string }

async function createUser(data: UserInput): Promise<Result<User>> {
  try {
    const user = await db.users.create(data)
    return { success: true, data: user }
  } catch (e) {
    return { success: false, error: "Failed to create user" }
  }
}

// Usage with type narrowing
const result = await createUser(input)
if (result.success) {
  console.log(result.data)  // TypeScript knows this is User
} else {
  console.error(result.error)  // TypeScript knows this is string
}

Utility Type Patterns

Partial and Required

Terminal
interface User {
  id: string
  name: string
  email: string
  age?: number
}

// All properties optional
type UserUpdate = Partial<User>
// { id?: string; name?: string; email?: string; age?: number }

// All properties required
type CompleteUser = Required<User>
// { id: string; name: string; email: string; age: number }

Pick and Omit

Terminal
// Select specific properties
type UserPreview = Pick<User, "id" | "name">
// { id: string; name: string }

// Exclude specific properties
type UserWithoutId = Omit<User, "id">
// { name: string; email: string; age?: number }

// Common pattern: Create without ID
type CreateUserInput = Omit<User, "id">

Record

Terminal
// Object with known key types
type UserRoles = Record<string, "admin" | "user" | "guest">
// { [key: string]: "admin" | "user" | "guest" }

const roles: UserRoles = {
  alice: "admin",
  bob: "user",
}

// Form errors
type FormErrors = Record<string, string>
const errors: FormErrors = {
  name: "Name is required",
  email: "Invalid email",
}

Exclude and Extract

Terminal
type Status = "pending" | "active" | "inactive" | "deleted"

// Remove specific values
type ActiveStatus = Exclude<Status, "deleted">
// "pending" | "active" | "inactive"

// Keep only matching values
type OnlyActive = Extract<Status, "active" | "pending">
// "active" | "pending"

Modifying AI-Generated Types

Adding Properties

Terminal
// AI generates:
interface User {
  id: string
  name: string
}

// You extend:
interface User {
  id: string
  name: string
  // Added
  avatar?: string
  role: "admin" | "user"
}

Making Optional Required

Terminal
// AI generates optional:
interface FormData {
  name?: string
  email?: string
}

// You make required:
interface FormData {
  name: string
  email: string
}

Adding Constraints

Terminal
// AI generates simple type:
interface Product {
  price: number
}

// You add validation with Zod:
const productSchema = z.object({
  price: z.number().positive().max(1000000)
})

Summary

  • Component props: Interface with HTML element extension
  • Hooks: Explicit generics for complex state
  • Events: Use React's event types (React.MouseEvent, etc.)
  • Next.js: Typed params, searchParams, and server actions
  • Zod: Schema validation with inferred types
  • Utility types: Partial, Pick, Omit, Record for transformations

Module Complete

You've learned TypeScript essentials for AI code:

  1. ✅ TypeScript fundamentals
  2. ✅ Common patterns in React/Next.js

Continue with Supabase to learn backend database and authentication.

Mark this lesson as complete to track your progress