Typed components
How a component's source narrows to one resource's record, and createResourceComponents, the factory that binds a resource into every field, input and view.
A source is a path into a record. What the path may be depends on which
record the component reads, and a plain component does not know. This page
covers the two ways to tell it: leave it on the registered union, or bind one
resource with createResourceComponents.
It assumes the app has registered its resources.
Each example below hides that setup above a ---cut--- line and shows the call
site.
Plain components
A component you import from the package takes every registered resource's paths:
import { TextInput } from "@chelone/react";
export const title = <TextInput source="title" />;Its source is AnySourcePath: the union of the paths of every registered
resource. A path that no resource has is an error, and a path that any resource
has compiles, even on a page about another one. AnySourcePath is string
when nothing is registered, and also when any registered resource has no
schema, since that resource's record names no paths.
To check a source against one resource, bind it.
createResourceComponents("posts")
createResourceComponents takes a resource name and hands back that resource's
components. It takes no type arguments: the name is checked against the
registry and the record is read from it. Call it at module scope.
import { createResourceComponents } from "@chelone/react";
const posts = createResourceComponents("posts");
export const PostEdit = () => (
<posts.Edit id={1}>
<posts.TextInput source="title" />
<posts.NumberInput source="views" />
</posts.Edit>
);In React a second call with the same name returns the same components, so a call inside a component body does not remount them. It is still wasted work, so keep it at module scope.
What it returns
The type is ResourceComponents, and it has two parts:
- The package's own fields and inputs, retyped and not wrapped.
posts.TextInput === TextInputholds at runtime. Only the type ofsource, and of a few callbacks below, changes. - The four views
posts.Edit,posts.Create,posts.Showandposts.List, bound to the name. They take noresourceprop, because the factory passes"posts"down.
The fields and inputs that are members include posts.TextField,
posts.NumberField, posts.DateField, posts.TextInput, posts.NumberInput
and posts.SelectArrayInput. The list is read from the source: React reads it
from the components object in create-resource-components.tsx, and Vue from
sourceMembers plus TypedFieldMembers and TypedInputMembers. A new field or
input that takes a source joins the list in both adapters.
How source narrows
Each member's source becomes SourceProp of that resource's record. Dot
paths work, and so do bracket paths into an array, such as tags[0].label. The
^? line shows what a post's source takes: its paths, the row paths of its
tags (label), and filter keys such as q:
const const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
views: z.ZodNumber;
tags: z.ZodArray<z.ZodObject<{
label: z.ZodString;
}, z.core.$strip>>;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts = createResourceComponents<"posts">(name: "posts"): ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
views: z.ZodNumber;
tags: z.ZodArray<z.ZodObject<{
label: z.ZodString;
}, z.core.$strip>>;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: 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");
type type PostSource = "id" | "title" | "views" | "tags" | "label" | `tags[${number}]` | `tags[${number}].label` | FilterSourceKey<{
id: number;
title: string;
views: number;
tags: {
label: string;
}[];
}>
PostSource = type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : neverObtain the parameters of a function type in a tupleParameters<typeof const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
views: z.ZodNumber;
tags: z.ZodArray<z.ZodObject<{
label: z.ZodString;
}, z.core.$strip>>;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts.type TextInput: <TPath extends _RecordSourcePath<{
id: number;
title: string;
views: number;
tags: {
label: string;
}[];
}>>(props: Omit<InputProps<unknown, "id" | "title" | "views" | "tags" | "label" | "body" | "q" | `tags[${number}]` | `tags[${number}].label` | `id_${string}` | `title_${string}` | `views_${string}` | `tags_${string}` | `tags[${number}]_${string}` | `tags[${number}].label_${string}` | `body_${string}`> & {
multiline?: boolean;
inputClassName?: string;
resettable?: boolean;
className?: string;
controlSuffix?: React.ReactNode;
} & Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "defaultValue" | ... 2 more ... | "type"> & Omit<...>, "transform" | ... 3 more ... | "parse"> & {
...;
} & {
...;
} & {
...;
} & {
...;
}) => JSX.Element
TextInput>[0]["source"];
A misspelt path is an error:
const const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts = createResourceComponents<"posts">(name: "posts"): ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: 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");
const const field: JSX.Elementfield = <const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts.type TextInput: <TPath extends _RecordSourcePath<{
id: number;
title: string;
}>>(props: Omit<InputProps<unknown, "id" | "title" | "body" | "q" | `id_${string}` | `title_${string}` | `body_${string}`> & {
multiline?: boolean;
inputClassName?: string;
resettable?: boolean;
className?: string;
controlSuffix?: React.ReactNode;
} & Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "defaultValue" | "onBlur" | "onChange" | "type"> & Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "defaultValue" | ... 2 more ... | "type">, "defaultValue" | ... 3 more ... | "transform"> & {
...;
} & {
...;
} & {
...;
} & {
...;
}) => JSX.Element
TextInput source="titel" />;So is a path that only another resource has. body is a path of comments,
and a plain <TextInput source="body" /> accepts it on a posts page. The bound
posts.TextInput does not:
const const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts = createResourceComponents<"posts">(name: "posts"): ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: 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");
const const field: JSX.Elementfield = <const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
readonly comments: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
body: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts.type TextInput: <TPath extends _RecordSourcePath<{
id: number;
title: string;
}>>(props: Omit<InputProps<unknown, "id" | "title" | "body" | "q" | `id_${string}` | `title_${string}` | `body_${string}`> & {
multiline?: boolean;
inputClassName?: string;
resettable?: boolean;
className?: string;
controlSuffix?: React.ReactNode;
} & Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "defaultValue" | "onBlur" | "onChange" | "type"> & Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "defaultValue" | ... 2 more ... | "type">, "defaultValue" | ... 3 more ... | "transform"> & {
...;
} & {
...;
} & {
...;
} & {
...;
}) => JSX.Element
TextInput source="body" />;A source that is optional on a component stays optional on the member. In
React, ReferenceManyCount declares source?, so
<posts.ReferenceManyCount reference="comments" target="post_id" /> compiles,
while <posts.TextInput /> does not.
Value callbacks
format, parse, transform and defaultValue take the value at that
source, which is SourceValue of the record and the path. On a views source
that is a number:
const const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
views: z.ZodNumber;
}, z.core.$strip>;
};
}, "posts">
posts = createResourceComponents<"posts">(name: "posts"): ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
views: z.ZodNumber;
}, 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");
export const const views: JSX.Elementviews = (
<const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
views: z.ZodNumber;
}, z.core.$strip>;
};
}, "posts">
posts.type NumberInput: <TPath extends _RecordSourcePath<{
id: number;
views: number;
}>>(props: Omit<NumberInputProps<unknown, _RecordSourcePath<{
id: number;
views: number;
}>>, "defaultValue" | "source" | "format" | "parse" | "transform"> & {
source: TPath;
} & {
defaultValue?: NoInfer<string extends TPath ? unknown : _Dotted<TPath> extends `${infer H}.${infer R}` ? _GetPath<_Index<{
id: number;
views: number;
}, H>, R> : _Index<{
id: number;
views: number;
}, _Dotted<TPath>>> | undefined;
} & {
...;
} & {
...;
}) => JSX.Element
NumberInput
source: "views"source="views"
format?: ((value: number) => string) | undefinedformat={(value: numbervalue) => var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String(value: numbervalue)}
/>
);A parse must return that value type, so parse={(value) => Number(value)} is
an error on a title source. A member whose component has none of the four
props keeps its own props.
Not members
The add-on components, which are monaco, maps, mdx, supabase and csv-import,
are not members. The import fence keeps them out of the main entry, so the
factory cannot hand them back. Import them from their own entry. Their source
keeps the registered AnySourcePath.
With no registration
When Register is not augmented, any name compiles 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";
const const comments: ResourceComponents<ResourceMap, "comments">comments = createResourceComponents<"comments">(name: "comments"): ResourceComponents<ResourceMap, "comments">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("comments");
const const field: JSX.Elementfield = <const comments: ResourceComponents<ResourceMap, "comments">comments.type TextInput: <TPath extends string>(props: Omit<InputProps<unknown, string> & {
multiline?: boolean;
inputClassName?: string;
resettable?: boolean;
className?: string;
controlSuffix?: React.ReactNode;
} & Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "defaultValue" | "onBlur" | "onChange" | "type"> & Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "defaultValue" | ... 2 more ... | "type">, "defaultValue" | ... 3 more ... | "transform"> & {
...;
} & {
...;
} & {
...;
} & {
...;
}) => JSX.Element
TextInput source: "anything"source="anything" />;The same code stays valid when the app registers later. What changes is that
"comments" has to be a registered name.
With comments missing from a registry that has posts only, the name is the
error:
const const comments: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
comments = 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("comments");createResourceComponentsOf
createResourceComponentsOf<typeof resources, "posts">("posts") writes both
type arguments, so it needs no Register augmentation. Register is global to
a TypeScript program, so this suits a type test or a program that must not
augment it. The returned type is the same ResourceComponents.
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 } from "@chelone/core";
import { function createResourceComponentsOf<TResources extends ResourceMap, TName extends keyof TResources & string>(name: TName): ResourceComponents<TResources, TName>The components of the resource `name`, typed by the record of that resource
in the map `TResources`. Use it where `Register` is not augmented, as a type
test does.createResourceComponentsOf } 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() }) },
});
const const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts = createResourceComponentsOf<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "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 map `TResources`. Use it where `Register` is not augmented, as a type
test does.createResourceComponentsOf<typeof const resources: {
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}
resources, "posts">("posts");
export const const title: JSX.Elementtitle = <const posts: ResourceComponents<{
readonly posts: {
readonly schema: z.ZodObject<{
id: z.ZodNumber;
title: z.ZodString;
}, z.core.$strip>;
};
}, "posts">
posts.type TextInput: <TPath extends _RecordSourcePath<{
id: number;
title: string;
}>>(props: Omit<InputProps<unknown, string> & {
multiline?: boolean;
inputClassName?: string;
resettable?: boolean;
className?: string;
controlSuffix?: React.ReactNode;
} & Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "defaultValue" | "onBlur" | "onChange" | "type"> & Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "defaultValue" | ... 2 more ... | "type">, "defaultValue" | ... 3 more ... | "transform"> & {
...;
} & {
...;
} & {
...;
} & {
...;
}) => JSX.Element
TextInput source: "title"source="title" />;Vue differences
createResourceComponents and createResourceComponentsOf take the same
arguments in @chelone/vue, and posts.TextInput is the package's own
component there too. Two things differ:
- The views keep
resourceasnever, so<posts.Edit resource="users">is a type error in a template undervue-tsc. React leaves the prop out. - A generic SFC that
TypedFieldMembersandTypedInputMembersdo not list is not a member.RecordFieldandSelectInputare such components. Import them directly. Theirsourceis on the registered union, as for a plain component.
<script setup lang="ts">
import { RecordField, SelectInput, createResourceComponents } from "@chelone/vue";
const posts = createResourceComponents("posts");
</script>
<template>
<posts.Edit :id="1">
<!-- A member: source is checked against posts. -->
<posts.TextInput source="title" />
<!-- Not members: source is any registered path. -->
<SelectInput source="status" :choices="[]" />
<RecordField source="author" />
<!-- Error: the view is bound to posts. -->
<posts.Edit resource="users" />
</posts.Edit>
</template>