Avatar — professional headshot 1
TutorialEngineering

TypeScript Patterns for Payload CMS 3: Type Safety from Database to UI

2 min

Why TypeScript Matters

Payload CMS 3 is written in 100% TypeScript. Every field generates corresponding types, giving you compile-time safety from schema to component props.

Auto-Generated Types

Run npx payload generate:types to generate payload-types.ts:

TypeScript
1// Auto-generated from your collection configs2export interface Post {3  id: number4  title: string5  slug: string6  content: {7    root: { type: 'root'; children: SerializedEditorState[] }8  }9  heroImage?: number | Media10  authors?: (number | User)[]11  publishedAt?: string12  _status?: 'draft' | 'published'13  createdAt: string14  updatedAt: string15}

Pattern 1: Typed Hooks

Collection Hooks

TypeScript
1import type { CollectionBeforeChangeHook } from 'payload'2 3export const generateSlug: CollectionBeforeChangeHook = async ({4  data,5  operation,6}) => {7  if (operation === 'create' && data.title && !data.slug) {8    data.slug = data.title9      .toLowerCase()10      .replace(/[^a-z0-9]+/g, '-')11      .replace(/(^-|-$)/g, '')12  }13  return data14}

Pattern 2: Typed Access Control

TypeScript
1import type { Access } from 'payload'2 3// Only authors can update their own posts4export const isAuthorOrAdmin: Access = ({ req: { user } }) => {5  if (!user) return false6  if (user.role === 'admin') return true7  return { authors: { contains: user.id } }8}

Pattern 3: Type-Safe Components

Import generated types in your React components. The compiler catches field mismatches:

TypeScript
1import type { Post } from '@/payload-types'2 3export const PostCard = ({ title, heroImage, publishedAt }: Post) => (4  <article>5    <h2>{title}</h2>6    {typeof heroImage === 'object' && (7      <img src={heroImage.url!} alt={heroImage.alt} />8    )}9    <time>{new Date(publishedAt!).toLocaleDateString()}</time>10  </article>11)

Pattern 4: Typed API Responses

The Local API returns typed results. Use depth to control relationship population:

TypeScript
1// depth: 0 → heroImage is a number (ID)2// depth: 1 → heroImage is a Media object3// depth: 2 → heroImage.sizes populated4const post = await payload.findByID({5  collection: 'posts',6  id: 1,7  depth: 2,8})9// post.heroImage is now fully typed as Media
TutorialEngineering

Related Posts