Register your layer types so Layer, actions, panes, and default layers stay type-safe across your app.
The designer is built around your layer types. Once you define those layer types, TypeScript can use them to narrow Layer, validate layer values, and catch invalid layer names before they reach the canvas.
The goal is to write the shape of each layer once, in your layerTypes array, and let the rest of the designer infer from it.
Registering Your Layer Types
Layer type registration has two parts:
- Create and export the
layerTypesarray that you pass to<Designer />. - Augment the
DesignerLayerTypesinterface with the inferred type map.
For example, if your app defines custom layers in src/layers, export the final array from a barrel file:
import { DEFAULT_LAYER_TYPES } from "@shadcn/designer";
import { shapeLayer } from "./shape";
export const layerTypes = [...DEFAULT_LAYER_TYPES, shapeLayer];Then add a declaration file that connects that runtime array to the designer's TypeScript registry:
import type { InferLayerTypes } from "@shadcn/designer";
import type { layerTypes } from "./layers";
declare module "@shadcn/designer" {
interface DesignerLayerTypes extends InferLayerTypes<typeof layerTypes> {}
}This follows a common TypeScript library pattern: your app augments a registry interface, and the library reads from that registry everywhere else.
declare module requires an interface — type aliases can't be merged into an existing module. If your linter flags this as an empty interface, configure @typescript-eslint/no-empty-object-type with allowInterfaces: "with-single-extends" for declaration files.
Make sure the declaration file is included by your tsconfig.json. In most Vite, Next.js, and TypeScript apps, a src/designer.d.ts file is picked up automatically.
Registering Keybindings
The same registry pattern is available for custom keybindings. Define your app's keybindings with the Keybinding type:
import { DEFAULT_KEYBINDINGS, type Keybinding } from "@shadcn/designer";
export const keybindings = {
...DEFAULT_KEYBINDINGS,
TOGGLE_PANEL_LEFT: {
key: "meta+b",
label: "Ctrl B",
labelMac: "⌘ B",
description: "Toggle Left Panel",
group: "Panels",
},
} satisfies Record<string, Keybinding>;Then add those names to the designer registry with InferKeybindings:
import type { InferKeybindings, InferLayerTypes } from "@shadcn/designer";
import type { keybindings } from "./keybindings";
import type { layerTypes } from "./layers";
declare module "@shadcn/designer" {
interface DesignerLayerTypes extends InferLayerTypes<typeof layerTypes> {}
interface DesignerKeybindings extends InferKeybindings<typeof keybindings> {}
}After that, APIs like useShortcut and <Shortcut /> accept your custom keybinding names:
useShortcut("TOGGLE_PANEL_LEFT", () => {
setLeftPanelOpen((open) => !open);
});Layer keybindings are also typed automatically. If you register a shape layer, ADD_LAYER_SHAPE becomes a valid KeybindingName.
What You Get
After registration, APIs in @shadcn/designer use your layer map:
Layer.typebecomes a union of literal strings, like"frame" | "group" | "text" | "image" | "shape".Layer.valuenarrows based ontype— TypeScript can discriminate values inswitch/ifstatements.Layer.metanarrows based ontype(when defined indefaultValues).showForLayerTypesonly accepts valid type names — typos are caught at compile time.defaultLayersvalidates that each layer'svaluematches itstype.KeybindingNameincludes built-in names, registered custom keybinding names, andADD_LAYER_*names for registered layers.
For example, if your shape layer uses value: { type: "square" }, a simple type guard gives you the typed value:
const layer = selectedLayers[0];
if (layer.type !== "shape") {
return null;
}
// layer.value is now typed as { type: string }.
console.log(layer.value.type);The same registration also protects layer-specific UI:
<DesignerPane showForLayerTypes={["shape"]}>
<DesignerPaneTitle>Shape</DesignerPaneTitle>
<DesignerPaneContent>
<ActionShapeType />
</DesignerPaneContent>
</DesignerPane>If you mistype "shape" here, TypeScript reports it instead of silently hiding the pane at runtime.
Layer value and meta setters use the same registry. If you pass narrowed layer objects, the new value is checked against those layers:
const shapeLayers = selectedLayers.filter((layer) => layer.type === "shape");
setLayersValue(shapeLayers, { type: "circle" });If you only have IDs, pass the layer type as a generic:
setLayersValue<"shape">(selectedLayerIds, { type: "square" });
setLayersMeta<"shape">(selectedLayerIds, { source: "toolbar" });InferLayerTypes
InferLayerTypes<T> walks your layerTypes array and produces the registry map the designer needs:
type AppLayerTypes = InferLayerTypes<typeof layerTypes>;Conceptually, that becomes:
type AppLayerTypes = {
shape: {
value: {
type: string;
};
};
};You typically don't need to use it directly — the snippet above is the only place it appears in most apps.
Common Issues
- If
Layer.typeis stillstring, check thatsrc/designer.d.tsis included by TypeScript and restart your editor's TypeScript server. - If custom layer names are missing from the union, make sure the same
layerTypesarray is exported fromsrc/layersand passed to<Designer layerTypes={layerTypes}>. - Avoid annotating
layerTypesas a wideLayerType[]; let TypeScript infer the array so literaltypenames are preserved.
Performance Tip
If your project has a large number of layer types, prefer narrowing with type guards (if (layer.type !== "shape") return) over wide unions in function signatures. Narrowing early gives TypeScript less work to do and keeps editor performance snappy as your project grows.
Next Steps
- See Layers for the full walkthrough of creating a custom layer with type-safe values.
- Learn how to use the unit system.
- Browse the type reference for
Layer,LayerType, and related types.