rilaykit
Core concepts

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:

lib/rilay.ts
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 the TextInputProps interface
  • 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 rilay instance 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 strict mode in your tsconfig.json for complete type safety

On this page