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:
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`.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`.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:
import {
function defineResources<const T extends ResourceMap>(resources: T & Checked<T>): TIdentity 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`.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`.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" />;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, theRegisteraugmentation, 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 thedefineResourcesmap and its augmentation from your database types. - Typed components — how a component's
props read the registry, and
createResourceComponentsbinding one resource's record into everysourceit 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 behindusePermissions. - When types widen — the fallbacks, and
why a resource with no schema, a
z.any()column or a jsonb column keeps taking anystring.