Skip to main content

Generated types

npx copy-ink types

An AST scan of your JSX, written to .copy-ink/:

FileContents
manifest.jsonEvery <Copy> usage — component, field, resolved scope, collection, item, locale, and source location — plus the route rendering each collection's items
copy-ink-env.d.tsModule augmentation: field, scope, collection and locale unions, plus each collection's item shape
image-hosts.jsonThe allowedHosts array, for next.config

Wiring it up

Add .copy-ink to your tsconfig include:

tsconfig.json
{
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".copy-ink"]
}

copy-ink doctor warns (tsconfig-include) when this is missing — without it the generated declarations are never loaded and autocomplete stays empty.

.copy-ink/ is generated output. Add it to .gitignore and regenerate in CI, or commit it — either works, but pick one.

What the declarations do

.copy-ink/copy-ink-env.d.ts
declare module 'copy-ink' {
interface CopyInkRegistry {
fields: 'heroTitle' | 'hero.subtitle' | 'body'
scopes: '' | 'about' | 'contact'
collections: 'posts'
locales: 'en' | 'de'
}

interface CopyInkCollectionData {
readonly posts: {
readonly title: string
readonly date?: string
readonly cover?: ImageNode
}
}
}

KnownField, KnownScope, KnownCollection and KnownLocale resolve from that registry. Before the file exists every slot is absent, so each one widens to plain string and nothing you write is rejected.

Fields come from three places: every <Copy> usage in your JSX, every path present in a YAML file (whether anything reads it or not), and every collection field in the config. meta and its children are excluded — that block is metadata, not copy.

Suggestions, not constraints

The unions are typed as Known | (string & {}). Autocomplete offers the known values; an unknown string still type-checks.

That is deliberate. A stale manifest — a field added to YAML after the last types run, a component written before the scan — would otherwise turn a working project red, and the developer's first instinct would be to stop running the tool.

For the check that does fail, run copy-ink check: it reads the same manifest and reports missing-key for a field JSX references but no file defines.

Why a static scan

A field inside a branch that never renders would never register at runtime, so runtime registration alone could not tell you it exists. Scanning the source sees every usage, whether or not it executes — which is what makes check trustworthy rather than merely encouraging.

The scan knows the difference between a scope it can resolve (a literal scope="about", or the route a file maps to) and one it cannot (a scope from a variable). Unresolvable entries record scope: null rather than guessing, and check does not report on them.

When to re-run

  • After adding or renaming a <Copy> usage
  • After adding a key to a content file
  • After changing collections or locales
  • After changing images.allowedHosts — plus a next dev restart

A predev / prebuild script keeps it honest:

package.json
{
"scripts": {
"predev": "copy-ink types",
"prebuild": "copy-ink types && copy-ink check"
}
}