Type safety
Carry contracts from definitions to consumers.
Owner: @bitakit/core types and discovery; React bindings consume the same contracts through @bitakit/ui.
BitaKit is designed around type-safe feature composition. Define the contract at its source, then let service access, action execution, events and bindings carry the types to callers.
| Boundary | What TypeScript knows |
|---|---|
defineContract('auth').of<T>() | The shared provider and consumer contract |
| Discovered service factory | Method arguments and return types of services.<plugin>.<file> |
| Discovered action definition | Action ID, input and completed result |
events.ts | Event names, payloads and which events are cancelable |
configs/ | The resolved config tree of each plugin through ConfigMap |
| Declared slots | Slot names through SlotMap |
Typed action inputs
import { defineAction } from '@bitakit/core';
import { z } from 'zod';
export default defineAction({
title: 'Create todo',
input: z.object({ title: z.string().min(1) }),
execute: ({ services, input }) => services.todos.list.add(input.title), // input is { title: string }
});With an input schema, the input and the result are inferred and the schema validates at runtime. Without one, name the input explicitly: defineAction<string>({ ... }); the result is then unknown, and defineAction<string, number> names both.
In a discovered contribution, the effective ID maps to that input and result in the generated ActionMap, so useAction('todos.create'), <Action id="todos.create" input={...}> and app.actions.todos.create.execute(...) autocomplete the ID and check the input. Keep the generated src/bitakit.gen.tsx in the host TypeScript project.
Strict names
Generated apps reject unknown names at compile time: discovery augments ActionMapOptions with { strict: true }, and that one switch covers every generated name.
| Name | Checked where |
|---|---|
| Action IDs and inputs | <Action id>, ActionButton, ActionLink, useAction, Action slots |
| Event names | events.emit, events.on, useEvent; on also accepts <plugin>.* patterns of declared events and * |
| Slot names | PluginSlot, useSlot |
| Setting keys | useSetting; settings.<plugin>.<name> in services and Actions |
| Service names | useService, useServiceState; services.<plugin>.<name> in services and Actions |
Installed package plugins add their record-form Actions and their events to the same maps. Set strict: false in bitakit.config.ts to allow dynamic names, or augment the option yourself in a manual composition:
// src/bitakit-types.d.ts
import '@bitakit/core';
declare module '@bitakit/core' {
interface ActionMapOptions {
strict: true;
}
}Typed services, events and config
Discovery derives ServiceMap from each service factory's return type, EventMap and CancelableEventMap from defineEvents in events.ts, and ConfigMap from defineConfig schemas or defaults. Manual compositions augment the same interfaces:
import '@bitakit/core';
declare module '@bitakit/core' {
interface ServiceMap {
'todos.list': { add(title: string): void; remove(id: number): void };
}
interface EventMap {
'todos.changed': { kind: 'added' | 'removed'; id: number };
}
}Known names require their declared payload; unknown names remain allowed with unknown data. These augmentations provide types, not runtime registration. requires separately lists services from other plugins, which BitaKit checks at startup.
Runtime boundaries
TypeScript does not validate network responses, stored JSON or user input. Parse untrusted values with the application's schema library — an action's input schema does this for action inputs — and keep authorization on the server. A config fallback is not a schema. Typed service access does not guarantee an optional provider is mounted; check app.has('auth') or declare the dependency.
This is why these guides identify the typed boundaries precisely rather than claiming every dynamic string or external response is automatically safe.