From 77a208fff76c0e7d0c6fc096e8381a30f37e1fb4 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 26 Nov 2021 17:10:01 -0500 Subject: [PATCH] docs: typescript --- demo/payload-types.ts | 98 +++++++++++------------ docs/fields/blocks.mdx | 9 +++ docs/fields/overview.mdx | 12 +++ docs/fields/rich-text.mdx | 11 +++ docs/hooks/collections.mdx | 23 ++++++ docs/hooks/fields.mdx | 28 +++++++ docs/hooks/globals.mdx | 17 ++++ docs/typescript/generating-types.mdx | 114 +++++++++++++++++++++++++++ docs/typescript/overview.mdx | 36 +++++++++ src/bin/generateTypes.ts | 4 + src/fields/config/types.ts | 4 +- types.d.ts | 20 ++++- 12 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 docs/typescript/generating-types.mdx create mode 100644 docs/typescript/overview.mdx diff --git a/demo/payload-types.ts b/demo/payload-types.ts index 93a09f882..29751ee95 100644 --- a/demo/payload-types.ts +++ b/demo/payload-types.ts @@ -57,7 +57,7 @@ export interface LocalizedPost { }[]; id?: string; blockName?: string; - blockType: "richTextBlock"; + blockType: 'richTextBlock'; }[]; } /** @@ -73,14 +73,14 @@ export interface BlocksGlobal { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; } @@ -113,7 +113,7 @@ export interface Admin { apiKeyIndex?: string; loginAttempts?: number; lockUntil?: string; - roles: ("admin" | "editor" | "moderator" | "user" | "viewer")[]; + roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[]; publicUser?: (string | PublicUser)[]; } /** @@ -126,11 +126,11 @@ export interface AllFields { descriptionText?: string; descriptionFunction?: string; image?: string | Media; - select: "option-1" | "option-2" | "option-3" | "option-4"; - selectMany: ("option-1" | "option-2" | "option-3" | "option-4")[]; + select: 'option-1' | 'option-2' | 'option-3' | 'option-4'; + selectMany: ('option-1' | 'option-2' | 'option-3' | 'option-4')[]; dayOnlyDateFieldExample: string; timeOnlyDateFieldExample?: string; - radioGroupExample: "option-1" | "option-2" | "option-3"; + radioGroupExample: 'option-1' | 'option-2' | 'option-3'; email?: string; number?: number; group?: { @@ -149,13 +149,13 @@ export interface AllFields { testEmail: string; id?: string; blockName?: string; - blockType: "email"; + blockType: 'email'; } | { testNumber: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; } | { author: string | PublicUser; @@ -163,14 +163,14 @@ export interface AllFields { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; relationship?: string | Conditions; @@ -178,11 +178,11 @@ export interface AllFields { relationshipMultipleCollections?: | { value: string | LocalizedPost; - relationTo: "localized-posts"; + relationTo: 'localized-posts'; } | { value: string | Conditions; - relationTo: "conditions"; + relationTo: 'conditions'; }; textarea?: string; richText: { @@ -258,13 +258,13 @@ export interface Conditions { testEmail: string; id?: string; blockName?: string; - blockType: "email"; + blockType: 'email'; } | { testNumber: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; } | { author: string | PublicUser; @@ -272,14 +272,14 @@ export interface Conditions { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; } @@ -297,13 +297,13 @@ export interface AutoLabel { testNumber?: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; }[]; noLabelBlock?: { testNumber?: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; }[]; items?: { itemName?: string; @@ -359,7 +359,7 @@ export interface File { filename?: string; mimeType?: string; filesize?: number; - type: "Type 1" | "Type 2" | "Type 3"; + type: 'Type 1' | 'Type 2' | 'Type 3'; owner: string | Admin; } /** @@ -370,9 +370,9 @@ export interface DefaultValueTest { id: string; text?: string; image?: string | Media; - select?: "option-1" | "option-2" | "option-3" | "option-4"; - selectMany?: ("option-1" | "option-2" | "option-3" | "option-4")[]; - radioGroupExample?: "option-1" | "option-2" | "option-3"; + select?: 'option-1' | 'option-2' | 'option-3' | 'option-4'; + selectMany?: ('option-1' | 'option-2' | 'option-3' | 'option-4')[]; + radioGroupExample?: 'option-1' | 'option-2' | 'option-3'; email?: string; number?: number; group?: { @@ -391,13 +391,13 @@ export interface DefaultValueTest { testEmail: string; id?: string; blockName?: string; - blockType: "email"; + blockType: 'email'; } | { testNumber: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; } | { author: string | PublicUser; @@ -405,14 +405,14 @@ export interface DefaultValueTest { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; relationship?: string | Conditions; @@ -420,11 +420,11 @@ export interface DefaultValueTest { relationshipMultipleCollections?: | { value: string | LocalizedPost; - relationTo: "localized-posts"; + relationTo: 'localized-posts'; } | { value: string | Conditions; - relationTo: "conditions"; + relationTo: 'conditions'; }; textarea?: string; slug?: string; @@ -444,13 +444,13 @@ export interface Blocks { testEmail: string; id?: string; blockName?: string; - blockType: "email"; + blockType: 'email'; } | { testNumber: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; } | { author: string | PublicUser; @@ -458,14 +458,14 @@ export interface Blocks { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; nonLocalizedLayout: ( @@ -473,13 +473,13 @@ export interface Blocks { testEmail: string; id?: string; blockName?: string; - blockType: "email"; + blockType: 'email'; } | { testNumber: number; id?: string; blockName?: string; - blockType: "number"; + blockType: 'number'; } | { author: string | PublicUser; @@ -487,14 +487,14 @@ export interface Blocks { color: string; id?: string; blockName?: string; - blockType: "quote"; + blockType: 'quote'; } | { label: string; url: string; id?: string; blockName?: string; - blockType: "cta"; + blockType: 'cta'; } )[]; } @@ -577,20 +577,20 @@ export interface RelationshipA { postLocalizedMultiple?: ( | { value: string | LocalizedPost; - relationTo: "localized-posts"; + relationTo: 'localized-posts'; } | { value: string | AllFields; - relationTo: "all-fields"; + relationTo: 'all-fields'; } | { value: number | CustomID; - relationTo: "custom-id"; + relationTo: 'custom-id'; } )[]; postManyRelationships?: { value: string | RelationshipB; - relationTo: "relationship-b"; + relationTo: 'relationship-b'; }; postMaxDepth?: string | RelationshipB; customID?: (number | CustomID)[]; @@ -606,20 +606,20 @@ export interface RelationshipB { postManyRelationships?: | { value: string | RelationshipA; - relationTo: "relationship-a"; + relationTo: 'relationship-a'; } | { value: string | Media; - relationTo: "media"; + relationTo: 'media'; }; localizedPosts?: ( | { value: string | LocalizedPost; - relationTo: "localized-posts"; + relationTo: 'localized-posts'; } | { value: string | PreviewablePost; - relationTo: "previewable-post"; + relationTo: 'previewable-post'; } )[]; strictAccess?: string | StrictAccess; @@ -654,10 +654,10 @@ export interface RichText { */ export interface Select { id: string; - Select: "one" | "two" | "three"; - SelectHasMany: ("one" | "two" | "three")[]; - SelectJustStrings: ("blue" | "green" | "yellow")[]; - Radio: "one" | "two" | "three"; + Select: 'one' | 'two' | 'three'; + SelectHasMany: ('one' | 'two' | 'three')[]; + SelectJustStrings: ('blue' | 'green' | 'yellow')[]; + Radio: 'one' | 'two' | 'three'; } /** * This interface was referenced by `Config`'s JSON-Schema diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index 2a48012ba..b6e7348f7 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -106,3 +106,12 @@ const ExampleCollection = { } ``` + +### TypeScript + +As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type: + +```js +import type { Block } from 'payload/types'; + +``` diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index d4ee50037..d1a01ad75 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -239,3 +239,15 @@ This example will display the number of characters allowed as the user types. } ``` This component will count the number of characters entered. + +### TypeScript + +You can import the internal Payload `Field` type as well as other common field types as follows: + +```js +import type { + Field, + Validate, + Condition, +} from 'payload/types'; +``` diff --git a/docs/fields/rich-text.mdx b/docs/fields/rich-text.mdx index fb93afb4b..f5be949b9 100644 --- a/docs/fields/rich-text.mdx +++ b/docs/fields/rich-text.mdx @@ -313,3 +313,14 @@ Above, you can see that we are creating a custom SlateJS element with a name of The plugin itself extends Payload's built-in `shouldBreakOutOnEnter` Slate function to add its own element name to the list of elements that should "break out" when the `enter` key is pressed. +### TypeScript + +If you are building your own custom Rich Text elements or leaves, you may benefit from importing the following types: + +```js +import type { + RichTextCustomElement, + RichTextCustomLeaf, +} from 'payload/types'; + +``` diff --git a/docs/hooks/collections.mdx b/docs/hooks/collections.mdx index 5049a59c2..ed4a299d6 100644 --- a/docs/hooks/collections.mdx +++ b/docs/hooks/collections.mdx @@ -188,3 +188,26 @@ const afterLoginHook = async ({ return user; } ``` + +## TypeScript + +Payload exports a type for each Collection hook which can be accessed as follows: + +```js +import type { + CollectionBeforeOperationHook, + CollectionBeforeValidateHook, + CollectionBeforeChangeHook, + CollectionAfterChangeHook, + CollectionAfterReadHook, + CollectionBeforeReadHook, + CollectionBeforeDeleteHook, + CollectionAfterDeleteHook, + CollectionBeforeLoginHook, + CollectionAfterLoginHook, + CollectionAfterForgotPasswordHook, +} from 'payload/types'; + +// Use hook types here... +} +``` diff --git a/docs/hooks/fields.mdx b/docs/hooks/fields.mdx index 3433e2571..6f5dc169f 100644 --- a/docs/hooks/fields.mdx +++ b/docs/hooks/fields.mdx @@ -70,3 +70,31 @@ All field hooks can optionally modify the return value of the field before the o Important
Due to GraphQL's typed nature, you should never change the type of data that you return from a field, otherwise GraphQL will produce errors. If you need to change the shape or type of data, reconsider Field Hooks and instead evaluate if Collection / Global hooks might suit you better. + +## TypeScript + +Payload exports a type for field hooks which can be accessed and used as follows: + +```js +import type { FieldHook } from 'payload/types'; + +// Field hook type is a generic that takes two arguments: +// 1: The document type +// 2: the value type + +type ExampleFieldHook = FieldHook; + +const exampleFieldHook: ExampleFieldHook = (args) => { + const { + value, // Typed as `string` as shown above + data, // Typed as a Partial of your ExampleDocumentType + originalDoc, // Typed as ExampleDocumentType + operation, + req, + } + + // Do something here... + + return value; // should return a string as typed above, undefined, or null +} +``` diff --git a/docs/hooks/globals.mdx b/docs/hooks/globals.mdx index cdfb919b7..6b2da6e67 100644 --- a/docs/hooks/globals.mdx +++ b/docs/hooks/globals.mdx @@ -98,3 +98,20 @@ const afterReadHook = async ({ req, // full express request }) => {...} ``` + +## TypeScript + +Payload exports a type for each Global hook which can be accessed as follows: + +```js +import type { + GlobalBeforeValidateHook, + GlobalBeforeChangeHook, + GlobalAfterChangeHook, + GlobalBeforeReadHook, + GlobalAfterReadHook, +} from 'payload/types'; + +// Use hook types here... +} +``` diff --git a/docs/typescript/generating-types.mdx b/docs/typescript/generating-types.mdx new file mode 100644 index 000000000..3240360b7 --- /dev/null +++ b/docs/typescript/generating-types.mdx @@ -0,0 +1,114 @@ +--- +title: Generating TypeScript Interfaces +label: Generating Types +order: 20 +desc: Generate your own TypeScript interfaces based on your collections and globals. +keywords: headless cms, typescript, documentation, Content Management System, cms, headless, javascript, node, react, express +--- + +While building your own custom functionality into Payload, like plugins, hooks, access control functions, custom routes, GraphQL queries / mutations, or anything else, you may benefit from generating your own TypeScript types dynamically from your Payload config itself. + +Run the following command in a Payload project to generate types: + +``` +payload generate:types +``` + +You can run this command whenever you need to regenerate your types, and then you can use these types in your Payload code directly. + +For example, let's look at the following simple Payload config: + +```ts +const config: Config = { + serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL, + admin: { + user: 'users', + } + collections: [ + { + slug: 'users', + fields: [ + { + name: 'name', + type: 'text', + required: true, + } + ] + }, + { + slug: 'posts', + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'author', + type: 'relationship', + relationTo: 'users', + }, + ] + } + ] +} +``` + +By generating types, we'll end up with a file containing the following two TypeScript interfaces: + +```ts +export interface User { + id: string; + name: string; + email?: string; + resetPasswordToken?: string; + resetPasswordExpiration?: string; + loginAttempts?: number; + lockUntil?: string; +} + +export interface Post { + id: string; + title?: string; + author?: string | User; +} + +``` + +#### Customizing the output path of your generated types + +You can specify where you want your types to be generated by adding a property to your Payload config: + +``` +{ + // the remainder of your config + typescript: { + outputFile: path.resolve(__dirname, './generated-types.ts'), + }, +} +``` + +The above example places your types next to your Payload config itself as the file `generated-types.ts`. By default, the file will be output to your current working directory as `payload-types.ts`. + +#### Adding an NPM script + + + Important:
+ Payload needs to be able to find your config to generate your types. +
+ +Payload will automatically try and locate your config, but might not always be able to find it. For example, if you are working in a `/src` directory or similar, you need to tell Payload where to find your config manually by using an environment variable. If this applies to you, you can create an NPM script to make generating your types easier. + +To add an NPM script to generate your types and show Payload where to find your config, open your `package.json` and update the `scripts` property to the following: + +``` +{ + "scripts": { + "generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", + }, +} +``` + +Now you can run `yarn generate:types` to easily generate your types. diff --git a/docs/typescript/overview.mdx b/docs/typescript/overview.mdx new file mode 100644 index 000000000..f9bfc7b78 --- /dev/null +++ b/docs/typescript/overview.mdx @@ -0,0 +1,36 @@ +--- +title: TypeScript - Overview +label: Overview +order: 10 +desc: Payload is the most powerful TypeScript headless CMS available. +keywords: headless cms, typescript, documentation, Content Management System, cms, headless, javascript, node, react, express +--- + +Payload supports TypeScript natively, and not only that, the entirety of the CMS is built with TypeScript. To get started developing with Payload and TypeScript, you can use one of Payload's built-in boilerplates in one line via `create-payload-app`: + +``` +npx create-payload-app +``` + +Pick a TypeScript project type to get started easily. + +#### Setting up from Scratch + +It's also possible to set up a TypeScript project from scratch. We plan to write up a guide for exactly how—so keep an eye out for that, too. + +## Using Payload's Exported Types + +Payload exports a number of types that you may find useful while writing your own plugins, hooks, access control functions, custom routes, GraphQL queries / mutations, or anything else. + +##### Config Types + +- [Base config](/docs/configuration/overview#typescript) +- [Collections](/docs/configuration/collections#typescript) +- [Globals](/docs/configuration/globals#typescript) +- [Fields](/docs/fields/overview#typescript) + +##### Hook Types + +- [Collection hooks](/docs/hooks/collections#typescript) +- [Global hooks](/docs/hooks/globals#typescript) +- [Field hooks](/docs/hooks/fields#typescript) diff --git a/src/bin/generateTypes.ts b/src/bin/generateTypes.ts index 32efbb4c1..5d3e92957 100644 --- a/src/bin/generateTypes.ts +++ b/src/bin/generateTypes.ts @@ -363,6 +363,10 @@ export function generateTypes(): void { compile(jsonSchema, 'Config', { unreachableDefinitions: true, + bannerComment: '/* tslint:disable */\n/**\n* This file was automatically generated by Payload CMS.\n* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,\n* and re-run `payload generate:types` to regenerate this file.\n*/', + style: { + singleQuote: true, + }, }).then((compiled) => { fs.writeFileSync(config.typescript.outputFile, compiled); payload.logger.info(`Types written to ${config.typescript.outputFile}`); diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index 539ecb79d..47e9adb86 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -14,9 +14,7 @@ export type FieldHookArgs = { req: PayloadRequest } -export type FieldHookReturnType = Promise | unknown; - -export type FieldHook = (args: FieldHookArgs) => FieldHookReturnType; +export type FieldHook = (args: FieldHookArgs) => Promise

| P; export type FieldAccess = (args: { req: PayloadRequest diff --git a/types.d.ts b/types.d.ts index 5f42216a2..1a6721b7e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -16,5 +16,21 @@ export { AfterForgotPasswordHook as CollectionAfterForgotPasswordHook, } from './dist/collections/config/types'; -export { GlobalConfig, SanitizedGlobalConfig } from './dist/globals/config/types'; -export { Field, FieldHook, FieldAccess, RichTextCustomElement, RichTextCustomLeaf, Block } from './dist/fields/config/types'; +export { + GlobalConfig, + SanitizedGlobalConfig, + BeforeValidateHook as GlobalBeforeValidateHook, + BeforeChangeHook as GlobalBeforeChangeHook, + AfterChangeHook as GlobalAfterChangeHook, + BeforeReadHook as GlobalBeforeReadHook, + AfterReadHook as GlobalAfterReadHook, +} from './dist/globals/config/types'; + +export { + Field, + FieldHook, + FieldAccess, + RichTextCustomElement, + RichTextCustomLeaf, + Block, +} from './dist/fields/config/types';