- Learn
- Stack Essentials
- TypeScript
- TypeScript Patterns in AI Code
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,Recordfor transformations
Module Complete
You've learned TypeScript essentials for AI code:
- ✅ TypeScript fundamentals
- ✅ Common patterns in React/Next.js
Continue with Supabase to learn backend database and authentication.
Mark this lesson as complete to track your progress