rilaykit
Core concepts

TypeScript Support

How RilayKit's type propagation system provides full autocompletion and compile-time safety from component registry to rendered fields.

RilayKit is built around type propagation: each registered component extends the catalog's { type → props } map on the ril generic, so form definitions are checked against your components at compile time.

Type Propagation

Step 1: Component Registration

Each .component() call extends the generic. TProps is inferred from the renderer's ComponentRenderContext<TProps> parameter (or from propsSchema):

lib/rilay.ts
import { ril, type ComponentRenderContext } from 'rilaykit';

interface TextInputProps {
  label: string;
  placeholder?: string;
}

interface SelectInputProps {
  label: string;
  options: Array<{ value: string; label: string }>;
}

function TextInput(ctx: ComponentRenderContext<TextInputProps>) { /* ... */ }
function SelectInput(ctx: ComponentRenderContext<SelectInputProps>) { /* ... */ }

export const rilay = ril
  .create()
  .component('text', { name: 'Text Input', renderer: TextInput })
  .component('select', { name: 'Select Input', renderer: SelectInput });

// rilay: ril<{ text: TextInputProps; select: SelectInputProps }>

Step 2: Form Building

In .add(), type autocompletes from the registered keys and props narrows to that component's own props — per argument, so a variadic .add(a, b) checks each field against its own type:

import { form, required, email } from 'rilaykit';
import { rilay } from '@/lib/rilay';

const loginForm = form.create(rilay, 'login')
  .add({
    id: 'email',
    type: 'text',                 // autocompletes: 'text' | 'select'
    props: { label: 'Email' },    // checked against Partial<TextInputProps>
    validation: { validate: [required(), email()] },
  });

props is typed Partial<TProps>: unknown keys and wrong value types are compile errors, but a missing required prop is not. Register a propsSchema on the component to catch that at runtime (rilay.validateProps(type, props), or compileForm(..., { validateProps: true })).

Error Prevention at Compile Time

form.create(rilay, 'test').add({
  type: 'checkbox',
  // Error: '"checkbox"' is not assignable to '"text" | "select"'
  props: { label: 'Accept' },
});

form.create(rilay, 'test').add({
  id: 'email',
  type: 'text',
  props: {
    label: 'Email',
    options: [],  // Error: 'options' does not exist on Partial<TextInputProps>
  },
});

Immutable API

ril is immutable — .component() returns a new instance with an extended type. Chain calls or assign the result; a discarded return value registers nothing:

const base = ril.create();
base.component('text', { renderer: TextInput });
// base is unchanged — 'text' was registered on a discarded instance

const rilay = ril.create().component('text', { renderer: TextInput });
// rilay: ril<{ text: TextInputProps }>

Workflows

Step definitions are type-checked too — formConfig accepts a built FormConfiguration or a form builder:

import { flow } from 'rilaykit';

const workflow = flow.create(rilay, 'onboarding', 'Onboarding')
  .step({
    id: 'personal',
    title: 'Personal Information',
    formConfig: loginForm,  // type-checked
  });

Best Practices

  • Define an explicit props interface per component and annotate the renderer's ComponentRenderContext<TProps> — inference flows from there
  • Export a single shared rilay instance from a central file (e.g. lib/rilay.ts)
  • Let TypeScript infer the generics — don't specify them manually
  • Enable strict mode in tsconfig.json

On this page