TypeScript Support
How RilayKit's type propagation system provides full autocompletion and compile-time safety from component registry to rendered fields.
Type safety is not an afterthought in RilayKit — it is the core design principle. The entire API is built around type propagation: types flow from your component definitions through form configurations to rendered fields, catching errors before your code ever runs.
Before and After
To understand the difference, compare building a form with manual typing versus RilayKit's type propagation.
// Types are disconnected from usage
interface FormData {
email: string;
password: string;
}
function LoginForm() {
const [values, setValues] = useState<FormData>({ email: '', password: '' });
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const validate = () => {
const newErrors: typeof errors = {};
if (!values.email) newErrors.email = 'Required';
if (!values.email.includes('@')) newErrors.email = 'Invalid email';
if (!values.password) newErrors.password = 'Required';
if (values.password.length < 8) newErrors.password = 'Too short';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
return (
<form onSubmit={() => validate() && handleSubmit(values)}>
{/* No connection between field IDs and types */}
<input value={values.email} onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
{errors.email && <span>{errors.email}</span>}
<input value={values.password} onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
{errors.password && <span>{errors.password}</span>}
</form>
);
}Issues: types are manually defined and disconnected from validation, no autocompletion for field IDs, component props are unchecked, validation logic is duplicated.
import { ril, required, email, minLength } from '@rilaykit/core';
import { Form, FormField } from '@rilaykit/forms';
// Types flow automatically from component registration
const rilay = ril.create()
.addComponent('input', { renderer: TextInput });
const loginForm = rilay.form('login')
.add({
id: 'email',
type: 'input', // Autocompleted from registry
props: { label: 'Email' }, // Typed as TextInputProps
validation: { validate: [required(), email()] },
})
.add({
id: 'password',
type: 'input',
props: { label: 'Password', type: 'password' },
validation: { validate: [required(), minLength(8)] },
});
// Render — fieldId autocompletes to 'email' | 'password'
<Form formConfig={loginForm} onSubmit={handleLogin}>
<FormField fieldId="email" />
<FormField fieldId="password" />
</Form>Every piece is connected: component types, props, field IDs, and validation are all verified at compile time.
Type Propagation
Type propagation is the mechanism by which TypeScript tracks your registered components and enforces their types throughout the API. It works through generic type accumulation on the ril instance.
Step 1: Component Registration
Each call to .addComponent() extends the generic type parameter of the ril instance:
import { ril } from '@rilaykit/core';
import { TextInput, SelectInput } from '@/components';
interface TextInputProps {
label: string;
placeholder?: string;
}
interface SelectInputProps {
label: string;
options: Array<{ value: string; label: string }>;
multiple?: boolean;
}
// Each .addComponent() call extends the type
export const rilay = ril
.create()
.addComponent('text', {
name: 'Text Input',
renderer: TextInput,
})
.addComponent('select', {
name: 'Select Input',
renderer: SelectInput,
});
// rilay is now typed as ril<{ text: TextInputProps; select: SelectInputProps }>Step 2: Form Building
When building forms, the accumulated types flow into .add():
const form = rilay
.form('user-form')
.add({
id: 'username',
type: 'text', // Autocompletes: 'text' | 'select'
props: {
label: 'Username',
placeholder: 'Enter your username', // Valid TextInputProps
},
});Step 3: Props Inference
Once you select a component type, the props object is automatically typed to match that component's interface:
.add({
id: 'country',
type: 'select',
props: {
label: 'Country',
options: [{ value: 'us', label: 'United States' }], // Required for SelectInputProps
multiple: true, // Available on SelectInputProps
},
})Error Prevention at Compile Time
RilayKit catches entire categories of bugs before your code runs.
Invalid Component Type
rilay.form('test').add({
type: 'checkbox',
// Error: Type '"checkbox"' is not assignable to type '"text" | "select"'
props: { label: 'Accept' },
});Fix: Register the component first with .addComponent('checkbox', { renderer: ... }).
Invalid Props for Component Type
rilay.form('test').add({
id: 'email',
type: 'text',
props: {
label: 'Email',
options: [], // Error: 'options' does not exist on TextInputProps
multiple: false, // Error: 'multiple' does not exist on TextInputProps
},
});Fix: Use props that match the component's interface, or use type: 'select' which accepts options.
Missing Required Props
rilay.form('test').add({
id: 'category',
type: 'select',
props: {
label: 'Category',
// Error: Property 'options' is missing in type
},
});Fix: Provide all required props defined in the component's interface.
Unused Return Value
const base = ril.create();
base.addComponent('text', { renderer: TextInput });
// 'base' still has no components — addComponent returns a NEW instance
base.form('test').add({
type: 'text', // Error: no component 'text' registered
});Fix: Always chain calls or assign the return value:
const rilay = ril.create().addComponent('text', { renderer: TextInput });Immutable API
The ril instance is immutable — each .addComponent() returns a new instance with an extended type. This ensures type safety across your application:
const base = ril.create();
// base is ril<Record<string, never>> — no components
const withText = base.addComponent('text', { renderer: TextInput });
// withText is ril<{ text: TextInputProps }>
const withBoth = withText.addComponent('select', { renderer: SelectInput });
// withBoth is ril<{ text: TextInputProps; select: SelectInputProps }>Because the API is immutable, always chain your .addComponent() calls or assign the final result. Calling .addComponent() without using the return value has no effect.
Multi-Field Rows
Type safety is maintained when adding multiple fields to the same row:
const form = rilay
.form('registration')
.add(
{
id: 'firstName',
type: 'text', // Typed as TextInputProps
props: { label: 'First Name' },
},
{
id: 'lastName',
type: 'text', // Typed as TextInputProps
props: { label: 'Last Name' },
}
);Workflows
Type safety extends to workflows. When building steps, the formConfig accepts both a FormConfiguration and a form builder instance:
const step1Form = rilay.form('personal-info')
.add({ id: 'name', type: 'text', props: { label: 'Name' } });
const workflow = rilay.flow('onboarding', 'Onboarding')
.addStep({
id: 'personal',
title: 'Personal Information',
formConfig: step1Form, // Type-checked
});IDE Experience
With RilayKit's type propagation system, your IDE provides:
- Autocompletion for component types, props, and field IDs
- Inline error highlighting for invalid types or props before running code
- Go to definition navigation from
type: 'text'to theTextInputPropsinterface - Rename refactoring — rename component types safely across the entire codebase
Best Practices
- Define explicit prop interfaces for each component — this gives the best autocompletion and documentation
- Use a single shared
rilayinstance exported from a central file (e.g.,lib/rilay.ts) - Let TypeScript infer the generic types — avoid manually specifying them
- Use TypeScript 5.0+ for the best type inference performance
- Enable
strictmode in yourtsconfig.jsonfor complete type safety