Type Safety

Chelone compiles loose. One file of registration is what turns a source from a plain string into a checked path.

Chelone types its own shapes exactly: a component's props, a hook's return, a data provider's payload. What it cannot know on its own is your app — which resources it has, what a record of each one holds, which catalog it translates against, what its permissions look like. Until you say, every one of those holes takes the loose answer: source is a string, useTranslate takes any key, usePermissions returns unknown.

That default is what lets an app mount <Admin> before it has written a single schema, and it is not a warning. It is also the whole cost of staying there: a misspelt field name compiles, and shows up as an empty column.

Loose until you register

An app that registers nothing. createResourceComponents hands back the components of a resource, and with no registry behind it every name is accepted and every source is a string:

src/posts.tsx
import { function createResourceComponents<TName extends RegisteredResourceName>(name: TName): ResourceComponents<RegisteredResources, TName>
The components of the resource `name`, typed by the record of that resource in the registered map. With nothing registered every name compiles and every `source` is a `string`.
@exampleconst posts = createResourceComponents("posts"); <posts.TextInput source="title" />
createResourceComponents
} from "@chelone/react";
// Nothing registered: every `source` is a plain `string`. const {
const TextField: <TPath extends string>(props: Omit<TextFieldProps<ResourceRecord<Identifier>>, "defaultValue" | "source" | "format" | "parse" | "transform"> & {
    source: TPath;
} & {
    defaultValue?: NoInfer<string extends TPath ? unknown : _Dotted<TPath> extends `${infer H}.${infer R}` ? _GetPath<_Index<NonNullable<LooseRecord>, H>, R> : _Index<NonNullable<LooseRecord>, _Dotted<TPath>>> | undefined;
} & {} & {}) => JSX.Element | null
TextField
} = createResourceComponents<"posts">(name: "posts"): ResourceComponents<ResourceMap, "posts">
The components of the resource `name`, typed by the record of that resource in the registered map. With nothing registered every name compiles and every `source` is a `string`.
@exampleconst posts = createResourceComponents("posts"); <posts.TextInput source="title" />
createResourceComponents
("posts");
const const field: JSX.Elementfield = <
const TextField: <TPath extends string>(props: Omit<TextFieldProps<ResourceRecord<Identifier>>, "defaultValue" | "source" | "format" | "parse" | "transform"> & {
    source: TPath;
} & {
    defaultValue?: NoInfer<string extends TPath ? unknown : _Dotted<TPath> extends `${infer H}.${infer R}` ? _GetPath<_Index<NonNullable<LooseRecord>, H>, R> : _Index<NonNullable<LooseRecord>, _Dotted<TPath>>> | undefined;
} & {} & {}) => JSX.Element | null
TextField
source: "titel"source="titel" />;

titel is a typo for title, and TypeScript agrees with it. Nothing in this snippet is wrong until the app says what its records hold.

Registered, the same line fails

Add a schema and one declare module block, and the same source="titel" is an error:

src/posts.tsx
import {
  function defineResources<const T extends ResourceMap>(resources: T & Checked<T>): T
Identity helper that pins literal resource names + schema types via `const`. `defineResources({ posts: { schema: postSchema } })` keeps `"posts"` literal and `typeof postSchema` exact, enabling end-to-end inference.
defineResources
,
type
type RegisteredRecord<TName extends RegisteredResourceName> = {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}[TName] extends {
    schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or {@link LooseRecord } when it has no schema.
RegisteredRecord
,
type
type SourceProp<TRecord = LooseRecord> = LooseRecord extends TRecord ? _RecordSourcePath<{
    id: number;
    title: string;
}> : _RecordSourcePath<TRecord>
The type a `source` prop takes. A loose record (the default, and every app that registers nothing) gets {@link AnySourcePath } , which is `string` until `Register` is augmented; a typed record gets its own paths.
SourceProp
,
} from "@chelone/core"; import { function createResourceComponents<TName extends RegisteredResourceName>(name: TName): ResourceComponents<RegisteredResources, TName>
The components of the resource `name`, typed by the record of that resource in the registered map. With nothing registered every name compiles and every `source` is a `string`.
@exampleconst posts = createResourceComponents("posts"); <posts.TextInput source="title" />
createResourceComponents
} from "@chelone/react";
import { import zz } from "zod"; const
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
=
defineResources<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}>(resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
} & Checked<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}>): {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
Identity helper that pins literal resource names + schema types via `const`. `defineResources({ posts: { schema: postSchema } })` keeps `"posts"` literal and `typeof postSchema` exact, enabling end-to-end inference.
defineResources
({
posts: {
    readonly schema: z.ZodObject<{
        id: z.ZodNumber;
        title: z.ZodString;
    }, z.core.$strip>;
} & {
    references?: {} | undefined;
}
posts
: {
schema: z.ZodObject<{
    id: z.ZodNumber;
    title: z.ZodString;
}, z.core.$strip>
schema
: import zz.
function object<{
    id: z.ZodNumber;
    title: z.ZodString;
}>(shape?: {
    id: z.ZodNumber;
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodNumber;
    title: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodNumberid: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber(), title: z.ZodStringtitle: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }) },
}); declare module "@chelone/core" { interface Register {
Register.resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
: typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
;
} } const {
const TextField: <TPath extends _RecordSourcePath<{
    id: number;
    title: string;
}>>(props: Omit<TextFieldProps<ResourceRecord<Identifier>>, "defaultValue" | "source" | "format" | "parse" | "transform"> & {
    source: TPath;
} & {
    defaultValue?: NoInfer<string extends TPath ? unknown : _Dotted<TPath> extends `${infer H}.${infer R}` ? _GetPath<_Index<{
        id: number;
        title: string;
    }, H>, R> : _Index<{
        id: number;
        title: string;
    }, _Dotted<TPath>>> | undefined;
} & {} & {}) => JSX.Element | null
TextField
} =
createResourceComponents<"posts">(name: "posts"): ResourceComponents<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}, "posts">
The components of the resource `name`, typed by the record of that resource in the registered map. With nothing registered every name compiles and every `source` is a `string`.
@exampleconst posts = createResourceComponents("posts"); <posts.TextInput source="title" />
createResourceComponents
("posts");
// The paths a `source` accepts now. type
type PostSource = "id" | "title" | FilterSourceKey<{
    id: number;
    title: string;
}>
PostSource
=
type SourceProp<TRecord = LooseRecord> = LooseRecord extends TRecord ? _RecordSourcePath<{
    id: number;
    title: string;
}> : _RecordSourcePath<TRecord>
The type a `source` prop takes. A loose record (the default, and every app that registers nothing) gets {@link AnySourcePath } , which is `string` until `Register` is augmented; a typed record gets its own paths.
SourceProp
<
type RegisteredRecord<TName extends RegisteredResourceName> = {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}[TName] extends {
    schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or {@link LooseRecord } when it has no schema.
RegisteredRecord
<"posts">>;
const const field: JSX.Elementfield = <
const TextField: <TPath extends _RecordSourcePath<{
    id: number;
    title: string;
}>>(props: Omit<TextFieldProps<ResourceRecord<Identifier>>, "defaultValue" | "source" | "format" | "parse" | "transform"> & {
    source: TPath;
} & {
    defaultValue?: NoInfer<string extends TPath ? unknown : _Dotted<TPath> extends `${infer H}.${infer R}` ? _GetPath<_Index<{
        id: number;
        title: string;
    }, H>, R> : _Index<{
        id: number;
        title: string;
    }, _Dotted<TPath>>> | undefined;
} & {} & {}) => JSX.Element | null
TextField
source="titel" />;
Type '"titel"' is not assignable to type '_RecordSourcePath<{ id: number; title: string; }>'. Did you mean '"title"'?

The augmentation is global to your TypeScript program: one file names the resources, and every component and hook in the app reads them off it. Hover the ^? line to see the paths source now accepts — the record's own fields, plus the filter keys a filter form writes (title_gte) and the free-text q.

Nothing about the loose shape is removed. A registered app still compiles the same code; what changed is that a name the registry has no record of stops compiling.

What registering types

Every fence in this section is a twoslash fence: the website build type-checks each one, so a snippet whose types drift fails the build instead of going stale.

  • Registering your app — the one file that does the registering: defineResources, the Register augmentation, and handing the map to <Admin resources={…}> in React and <Admin :registry="…"> in Vue.
  • Resource registry and codegen — the registry as a runtime value, and chelone-supabase gen, which writes the defineResources map and its augmentation from your database types.
  • Typed components — how a component's props read the registry, and createResourceComponents binding one resource's record into every source it takes.
  • Typed hooks — the hooks that read the same registry: what a record type arrives as, and what a hook returns for a registered resource.
  • Sources, filters and sort — the path grammar behind source: dotted paths, bracket indices, row-relative paths, and the operator-suffixed keys filters and sort buttons take.
  • Translations and permissions — the other two things you can register: the message catalog behind useTranslate, and the permission type behind usePermissions.
  • When types widen — the fallbacks, and why a resource with no schema, a z.any() column or a jsonb column keeps taking any string.