Skip to content

Commit abe8563

Browse files
authored
docs: adds strongly typed examples (#14513)
Fixes #14135
1 parent 015b363 commit abe8563

File tree

4 files changed

+129
-21
lines changed

4 files changed

+129
-21
lines changed

docs/access-control/collections.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ As your application becomes more complex, you may want to define your function i
132132

133133
```ts
134134
import type { Access } from 'payload'
135+
import type { Page } from '@/payload-types'
135136

136-
export const canReadPage: Access = ({ req: { user } }) => {
137+
export const canReadPage: Access<Page> = ({ req: { user } }) => {
137138
// Allow authenticated users
138139
if (user) {
139140
return true
@@ -187,8 +188,9 @@ As your application becomes more complex, you may want to define your function i
187188

188189
```ts
189190
import type { Access } from 'payload'
191+
import type { User } from '@/payload-types'
190192

191-
export const canUpdateUser: Access = ({ req: { user }, id }) => {
193+
export const canUpdateUser: Access<User> = ({ req: { user }, id }) => {
192194
// Allow users with a role of 'admin'
193195
if (user.roles && user.roles.some((role) => role === 'admin')) {
194196
return true
@@ -232,8 +234,9 @@ As your application becomes more complex, you may want to define your function i
232234

233235
```ts
234236
import type { Access } from 'payload'
237+
import type { Customer } from '@/payload-types'
235238

236-
export const canDeleteCustomer: Access = async ({ req, id }) => {
239+
export const canDeleteCustomer: Access<Customer> = async ({ req, id }) => {
237240
if (!id) {
238241
// allow the admin UI to show controls to delete since it is indeterminate without the `id`
239242
return true

docs/hooks/collections.mdx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,11 @@ Please do note that this does not run before client-side validation. If you rend
100100

101101
```ts
102102
import type { CollectionBeforeValidateHook } from 'payload'
103+
import type { Post } from '@/payload-types'
103104

104-
const beforeValidateHook: CollectionBeforeValidateHook = async ({ data }) => {
105+
const beforeValidateHook: CollectionBeforeValidateHook<Post> = async ({
106+
data, // Typed as Partial<Post>
107+
}) => {
105108
return data
106109
}
107110
```
@@ -123,8 +126,11 @@ Immediately before validation, beforeChange hooks will run during create and upd
123126

124127
```ts
125128
import type { CollectionBeforeChangeHook } from 'payload'
129+
import type { Post } from '@/payload-types'
126130

127-
const beforeChangeHook: CollectionBeforeChangeHook = async ({ data }) => {
131+
const beforeChangeHook: CollectionBeforeChangeHook<Post> = async ({
132+
data, // Typed as Partial<Post>
133+
}) => {
128134
return data
129135
}
130136
```
@@ -146,8 +152,12 @@ After a document is created or updated, the `afterChange` hook runs. This hook i
146152

147153
```ts
148154
import type { CollectionAfterChangeHook } from 'payload'
155+
import type { Post } from '@/payload-types'
149156

150-
const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => {
157+
const afterChangeHook: CollectionAfterChangeHook<Post> = async ({
158+
doc, // Typed as Post
159+
previousDoc, // Typed as Post
160+
}) => {
151161
return doc
152162
}
153163
```
@@ -170,8 +180,11 @@ Runs before `find` and `findByID` operations are transformed for output by `afte
170180

171181
```ts
172182
import type { CollectionBeforeReadHook } from 'payload'
183+
import type { Post } from '@/payload-types'
173184

174-
const beforeReadHook: CollectionBeforeReadHook = async ({ doc }) => {
185+
const beforeReadHook: CollectionBeforeReadHook<Post> = async ({
186+
doc, // Typed as Post
187+
}) => {
175188
return doc
176189
}
177190
```
@@ -192,8 +205,11 @@ Runs as the last step before documents are returned. Flattens locales, hides pro
192205

193206
```ts
194207
import type { CollectionAfterReadHook } from 'payload'
208+
import type { Post } from '@/payload-types'
195209

196-
const afterReadHook: CollectionAfterReadHook = async ({ doc }) => {
210+
const afterReadHook: CollectionAfterReadHook<Post> = async ({
211+
doc, // Typed as Post
212+
}) => {
197213
return doc
198214
}
199215
```
@@ -497,3 +513,17 @@ import type {
497513
CollectionMeHook,
498514
} from 'payload'
499515
```
516+
517+
You can also pass a generic type to each hook for strongly-typed `doc`, `previousDoc`, and `data` properties:
518+
519+
```ts
520+
import type { CollectionAfterChangeHook } from 'payload'
521+
import type { Post } from '@/payload-types'
522+
523+
const afterChangeHook: CollectionAfterChangeHook<Post> = async ({
524+
doc, // Typed as Post
525+
previousDoc, // Typed as Post
526+
}) => {
527+
return doc
528+
}
529+
```

docs/hooks/fields.mdx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,58 @@ const exampleFieldHook: ExampleFieldHook = (args) => {
264264
return value // should return a string as typed above, undefined, or null
265265
}
266266
```
267+
268+
### Practical Example with Generated Types
269+
270+
Here's a real-world example using generated Payload types:
271+
272+
```ts
273+
import type { FieldHook } from 'payload'
274+
import type { Post } from '@/payload-types'
275+
276+
// Hook for a text field in a Post collection
277+
type PostTitleHook = FieldHook<Post, string, Post>
278+
279+
const slugifyTitle: PostTitleHook = ({
280+
value,
281+
data,
282+
siblingData,
283+
originalDoc,
284+
}) => {
285+
// value is typed as string | undefined
286+
// data is typed as Partial<Post>
287+
// siblingData is typed as Partial<Post>
288+
// originalDoc is typed as Post | undefined
289+
290+
// Generate slug from title if not provided
291+
if (!siblingData.slug && value) {
292+
const slug = value
293+
.toLowerCase()
294+
.replace(/[^\w\s-]/g, '')
295+
.replace(/\s+/g, '-')
296+
297+
return value
298+
}
299+
300+
return value
301+
}
302+
303+
// Hook for a relationship field
304+
type PostAuthorHook = FieldHook<Post, string | number, Post>
305+
306+
const setDefaultAuthor: PostAuthorHook = ({ value, req }) => {
307+
// value is typed as string | number | undefined
308+
// Set current user as author if not provided
309+
if (!value && req.user) {
310+
return req.user.id
311+
}
312+
313+
return value
314+
}
315+
```
316+
317+
<Banner type="success">
318+
**Tip:** When defining field hooks, use the three generic parameters for full
319+
type safety: document type, field value type, and sibling data type. This
320+
provides autocomplete and type checking for all hook arguments.
321+
</Banner>

docs/hooks/globals.mdx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,12 @@ Please do note that this does not run before client-side validation. If you rend
8686

8787
```ts
8888
import type { GlobalBeforeValidateHook } from 'payload'
89+
import type { SiteSettings } from '@/payload-types'
8990

90-
const beforeValidateHook: GlobalBeforeValidateHook = async ({
91-
data,
91+
const beforeValidateHook: GlobalBeforeValidateHook<SiteSettings> = async ({
92+
data, // Typed as Partial<SiteSettings>
9293
req,
93-
originalDoc,
94+
originalDoc, // Typed as SiteSettings
9495
}) => {
9596
return data
9697
}
@@ -112,11 +113,12 @@ Immediately following validation, `beforeChange` hooks will run within the `upda
112113

113114
```ts
114115
import type { GlobalBeforeChangeHook } from 'payload'
116+
import type { SiteSettings } from '@/payload-types'
115117

116-
const beforeChangeHook: GlobalBeforeChangeHook = async ({
117-
data,
118+
const beforeChangeHook: GlobalBeforeChangeHook<SiteSettings> = async ({
119+
data, // Typed as Partial<SiteSettings>
118120
req,
119-
originalDoc,
121+
originalDoc, // Typed as SiteSettings
120122
}) => {
121123
return data
122124
}
@@ -138,13 +140,14 @@ After a global is updated, the `afterChange` hook runs. Use this hook to purge c
138140

139141
```ts
140142
import type { GlobalAfterChangeHook } from 'payload'
143+
import type { SiteSettings } from '@/payload-types'
141144

142-
const afterChangeHook: GlobalAfterChangeHook = async ({
143-
doc,
144-
previousDoc,
145+
const afterChangeHook: GlobalAfterChangeHook<SiteSettings> = async ({
146+
doc, // Typed as SiteSettings
147+
previousDoc, // Typed as SiteSettings
145148
req,
146149
}) => {
147-
return data
150+
return doc
148151
}
149152
```
150153

@@ -187,12 +190,15 @@ Runs as the last step before a global is returned. Flattens locales, hides prote
187190

188191
```ts
189192
import type { GlobalAfterReadHook } from 'payload'
193+
import type { SiteSettings } from '@/payload-types'
190194

191-
const afterReadHook: GlobalAfterReadHook = async ({
192-
doc,
195+
const afterReadHook: GlobalAfterReadHook<SiteSettings> = async ({
196+
doc, // Typed as SiteSettings
193197
req,
194198
findMany,
195-
}) => {...}
199+
}) => {
200+
return doc
201+
}
196202
```
197203

198204
The following arguments are provided to the `afterRead` hook:
@@ -219,3 +225,17 @@ import type {
219225
GlobalAfterReadHook,
220226
} from 'payload'
221227
```
228+
229+
You can also pass a generic type to each hook for strongly-typed `doc`, `previousDoc`, and `data` properties:
230+
231+
```ts
232+
import type { GlobalAfterChangeHook } from 'payload'
233+
import type { SiteSettings } from '@/payload-types'
234+
235+
const afterChangeHook: GlobalAfterChangeHook<SiteSettings> = async ({
236+
doc, // Typed as SiteSettings
237+
previousDoc, // Typed as SiteSettings
238+
}) => {
239+
return doc
240+
}
241+
```

0 commit comments

Comments
 (0)