Type Safety

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:

  1. Create and export the layerTypes array that you pass to <Designer />.
  2. Augment the DesignerLayerTypes interface with the inferred type map.

For example, if your app defines custom layers in src/layers, export the final array from a barrel file:

src/layers/index.ts
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:

src/designer.d.ts
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.

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:

src/keybindings.ts
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:

src/designer.d.ts
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.type becomes a union of literal strings, like "frame" | "group" | "text" | "image" | "shape".
  • Layer.value narrows based on type — TypeScript can discriminate values in switch/if statements.
  • Layer.meta narrows based on type (when defined in defaultValues).
  • showForLayerTypes only accepts valid type names — typos are caught at compile time.
  • defaultLayers validates that each layer's value matches its type.
  • KeybindingName includes built-in names, registered custom keybinding names, and ADD_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.type is still string, check that src/designer.d.ts is included by TypeScript and restart your editor's TypeScript server.
  • If custom layer names are missing from the union, make sure the same layerTypes array is exported from src/layers and passed to <Designer layerTypes={layerTypes}>.
  • Avoid annotating layerTypes as a wide LayerType[]; let TypeScript infer the array so literal type names 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