Skip to content
LogoLogo

Type Generation

Picked can generate TypeScript types from your workspace schema so field values, feature keys, content models, and step layouts are typed in your app.

Quick start

1. Add picked.config.ts

Create a picked.config.ts in your project root:

import { defineConfig } from 'picked/config'
 
export default defineConfig({
  projectId: 'your-project-id',
  typesOutput: './src/picked-env.d.ts',
})
OptionDescription
projectIdRequired. Your Picked project id.
typesOutputPath for generated types (defaults to ./src/picked-env.d.ts).
sourceSchema surface to type against: 'published' (default) or 'preview'.
previewTokenWorkspace preview token. Required when source is 'preview', and read only then.

The source value matches the x-picked-source field on the workspace schema document — it controls whether tooling fetches the published or preview workspace shape.

A preview token reads one workspace, and it does not expire. Leave both source and previewToken out of the committed file, and pick a workspace instead:

picked login
picked workspace use

That writes the choice, and that workspace's token, to .picked/settings.json, which is per-developer and which Git ignores. Each developer points at their own workspace, and the committed file holds no token.

You can also set the values here. Every developer then reads the same workspace:

export default defineConfig({
  projectId: 'your-project-id',
  source: 'preview',
  previewToken: 'the-workspace-preview-token',
})

Three places can give the token. Tooling reads the first of them that has one: the PICKED_PREVIEW_TOKEN environment variable, then the workspace that picked workspace use chose, then this previewToken option.

If you use Vite, add @picked/vite-plugin-picked. Type generation runs automatically on dev and build when generateTypes is enabled (the default):

import { picked } from '@picked/vite-plugin-picked'
 
export default defineConfig({
  plugins: [picked()],
})

The project id comes from picked.config.ts. A failure stops the run. See the plugin reference for generateTypes and every other option.

3. Or use the CLI

Install picked and run:

picked generate types

Keep the types current

Commit picked-env.d.ts. A workspace changes after you commit it, so the file goes stale, and stale types describe fields your workspace no longer has.

Run this check in CI, next to your other checks:

picked generate types --check

--check writes no file. It reads the live workspace schema and compares it with the committed file. A match gives status 0. A difference gives status 1 and names the file:

Types are out of date: /home/runner/work/your-app/src/picked-env.d.ts

To clear it, run picked generate types and commit the result. The check reads no cached schema, so an API it cannot reach fails the check.

Workspace selection

Tooling reads projectId and source from your committed picked.config.ts. The Picked CLI points that tooling at a different project or a draft workspace, and leaves the file as it is. It writes the selection to .picked/settings.json, which is per-developer and which Git ignores. That selection supersedes the committed default.

See How to switch workspaces for the full set of steps.

Generated output

The generated picked-env.d.ts file looks like:

// GENERATED by `@picked/typegen` — do not edit by hand.
// Reference: `https://docs.picked.app/reference/sdk/type-generation`
 
export type WelcomePage = { /* ... */ }
export type FieldValues = { /* ... */ }
export type FeatureKey =
  | 'default'
  | 'hero'
  | 'winter-sale'
 
declare module '@picked/client' {
  interface PickedSchemaRegistry {
    contentModels: {
      'welcome-page': WelcomePage
    }
    stepContentModels: {
      'intro': 'welcome-page'
    }
    featureValues: {
      'hero':
        | false
        | 'hero-a'
        | 'hero-b'
    }
    fieldValues: FieldValues
    featureKeys: FeatureKey
  }
}

Commit this file to the version control system, or regenerate it in CI after you publish a workspace change.

Feature flag values

A Text feature flag declares the values that it can serve, so useFeatureResult narrows the bucket to those values:

const result = useFeatureResult('hero')

The bucket property then has the type false | 'hero-a' | 'hero-b'.

featureValues holds the Text feature flags alone. A Boolean feature flag, and a feature flag that your application defines, both keep the general boolean | string type. The FeatureValueFor<'hero'> type gives the same union on its own.

Assets

An asset field is a union. An asset that the API finds has a url, and an asset that it does not find has no url. Field values gives the two shapes. The in operator narrows the union:

const image = page.hero_image
 
return 'url' in image ? <img src={image.url} width={image.width ?? undefined} alt="" /> : null

Content blocks

Each block type has a contentBlockKey property whose type is the key of the block. A switch on that property gives the data type of each block:

for (const section of page.sections ?? []) {
  switch (section.contentBlockKey) {
    case 'faq':
      renderFaq(section.data.items)
      break
    case 'hero':
      renderHero(section.data.headline)
      break
  }
}

The model of a record

isContentOfModel(record, key) from @picked/client returns true when the contentModelKey of the record is key. In that branch, record.data has the generated type of the model. ContentRecordOf<'landing-page'> gives the same record type, and ContentModelKeys holds the keys of the models in the workspace.

In this example, one component renders the layouts of two models:

import { isContentOfModel } from '@picked/client'
import { useStepLayoutContent } from '@picked/react/hooks'
 
export function Page() {
  const record = useStepLayoutContent()
 
  if (record && isContentOfModel(record, 'landing-page')) {
    return <LandingPage content={record.data} />
  }
 
  if (record && isContentOfModel(record, 'standard-page')) {
    return <StandardPage content={record.data} />
  }
 
  return null
}

The check reads contentModelKey only. It does not validate data against the model.

Keys the application defines

An application can define a feature, an action, a field, a step, or an experience of its own. It does this through createClient options or through a register method. The workspace has no record of such a key, so the generated types have none either.

A hook still accepts one. Each key type holds the keys of the workspace and any other string. Your editor shows the workspace keys, and no local key is an error:

useFeature('winter-sale')             // from the workspace, and suggested
useFeature('summer-sales-experiment') // from createClient, and accepted

Each key type has a Known form that holds the workspace keys alone — KnownFeatureKeys, KnownActionKeys, KnownFieldKeys, KnownStepKeys, and KnownExperienceKeys. Use one where you want the workspace set and nothing else.

Cache

Type generation writes its artefacts under .picked/: the schema JSON and a manifest. The first run adds this directory to .gitignore for you.