--- title: "Docs | Decorators" description: "Overview of the Radiant decorator surface across RadiantElement and RadiantController." group: Decorators order: 999 --- # Decorators Radiant decorators fall into a few groups: - host registration and HTML input: [@customElement](/docs/decorators/custom-element), [@controller](/docs/decorators/controller), [@attr](/docs/decorators/attr), [@prop](/docs/decorators/prop) - reactive state and bindings: [@state](/docs/decorators/state), [@signal](/docs/decorators/signal), [@bindTo](/docs/decorators/bind-to), [@onUpdated](/docs/decorators/on-updated) - DOM and event wiring: [@onEvent](/docs/decorators/on-event), [@event](/docs/decorators/event), [@query](/docs/decorators/query), [@querySlot](/docs/decorators/query-slot) - method helpers: [@bound](/docs/decorators/bound), [@debounce](/docs/decorators/debounce) ## Host Support - `RadiantElement` supports the full custom-element decorator surface. - `RadiantController` supports the controller-oriented subset plus render-driven state and context authoring. For the controller capability matrix, see [RadiantController](/docs/components/radiant-controller). ## Choosing A Reactive Field Type - use [@attr](/docs/decorators/attr) when the value should come from markup - use [@prop](/docs/decorators/prop) when the value is public API on a host - use [@state](/docs/decorators/state) for plain internal host state - use [@signal](/docs/decorators/signal) when the field itself should be a writable signal ## Typing Decorated Fields Under standard decorators, TypeScript passes each field decorator the declared field type, and the decorator checks its options, or the value it installs, against it. The field type is the source of truth: `defaultValue`, `initial`, `source`, `transform`, `converter` and `type` must agree with it, and a mismatch is a compile error on the decorator. Legacy decorators, covered at the end of this section, cannot run these checks. ### Declare The Type Prefer an initializer, so TypeScript infers the field type and there is no annotation to keep in sync: ```ts import { RadiantElement, customElement, prop, query, signal } from '@ecopages/radiant'; import type { WritableSignal } from '@ecopages/signals'; type Size = 'sm' | 'md' | 'lg'; @customElement('size-picker') export class SizePicker extends RadiantElement { @prop({ type: Number, defaultValue: 0 }) count = 0; @prop({ type: String, defaultValue: 'md' }) size: Size = 'md'; @prop({ type: Boolean }) open?: boolean; @signal({ initial: 0 }) clicks!: WritableSignal; @query({ ref: 'input' }) input!: HTMLInputElement | null; } ``` Write the type yourself for: - a string literal union, such as `size: Size = 'md'` - a field with no initializer: `?` when the value can be absent, which adds `undefined`, and `!` when the decorator or a default supplies it, which only silences the missing-initializer error - `@signal`, `@query` and the other fields a decorator installs, which have no initializer to infer from Declare a decorated field as a plain field. TypeScript rejects `declare` on it under standard decorators (TS1206), and an `accessor` field is not a target of these decorators. A field that disagrees with an option is rejected on the decorator. TypeScript may report it only as "Unable to resolve signature of property decorator", or alongside an error that names the option: ```ts @prop({ type: Number, defaultValue: 0 }) count: string = ''; // defaultValue is a number @prop({ type: Number }) step?: string; // Number parses to a number @attr({ source: 'data-total' }) total = 0; // the raw attribute is a string @signal({ initial: 'x' }) label!: WritableSignal; // initial is a string @query({ ref: 'input' }) input!: HTMLInputElement; // null when nothing matches ``` ### What Each Decorator Expects - [@state](/docs/decorators/state): any type. It is the type [@bindTo](/docs/decorators/bind-to) passes to `map`. - [@signal](/docs/decorators/signal): `WritableSignal`, with `initial` a `T` and `source` a `WritableSignal`, or a function returning one. - [@bindTo](/docs/decorators/bind-to): `map` receives the field type, so `@bindTo({ attr: 'data-n', map: (v) => v.toFixed() }) @state n = 0` types `v` as `number`. On a `@signal` field declared `WritableSignal` it also types `v` as `number`, the value the runtime passes. - [@query](/docs/decorators/query) and [@querySlot](/docs/decorators/query-slot): `T | null` for a single element, `T[]` with `all: true`. `all` must be a literal `true` or `false`. - [@consumeContext](/docs/context/consume-context): `ContextProvider`, which may be optional but not wider. - [@provideContext](/docs/context/provide-context): `ContextProvider`. The provider is always installed, so the field cannot be optional or wider. - [@event](/docs/decorators/event): `EventEmitter`. The emitter is always installed, so the field cannot be optional. - [@onContextUpdate](/docs/context/on-context-update): a method whose parameter accepts the value `select` returns, or the whole context without `select`. - [@contextSelector](/docs/context/context-selector): the value `select` returns, or the whole context without `select`. The field may be optional or wider, such as `unknown`, but not narrower: it has to hold what the decorator writes. ### @prop And @attr - The `type` constant must fit the field: `String` needs a `string`, `Number` a `number`, `Boolean` a `boolean`, `Array` an array (readonly included) and `Object` an object that is not an array. `null` and `undefined` are always allowed. A field of several parsed types, such as `string | number`, fits no constant. - A parser owns the value type: `transform.fromAttribute` on `@prop`, `converter.fromAttribute` on `@attr`. `type` is then not compared with the field, and `defaultValue` is checked against the field, not against `type`. On `@prop`, `type` still selects the value used when no attribute is set and neither `defaultValue` nor an initializer is given, such as `''` for `String`. Pass the parser inline or declare it with `satisfies PropTransform` (`AttrConverter` on `@attr`): an annotated declaration makes `fromAttribute` optional in its type, so `type` is checked again. - `@attr` without `type` or a parser holds the raw attribute string, so its field must be `string | null | undefined`. A string literal union fits. Any other field needs `type` or a parser. - `defaultValue: null` is accepted when the field allows `null`. Without a parser, a `defaultValue` that does not match `type` throws: when `@prop` is applied to the class, and when a host installs an `@attr`. ```ts @prop({ type: String, transform: { fromAttribute: (v) => Number(v) } }) rank = 0; @attr({ converter: { fromAttribute: (v) => (v ? v.split(',') : []) } }) labels: string[] = []; @attr({ source: 'data-count', type: Number }) count = 0; @prop({ type: Number, defaultValue: null }) limit: number | null = null; ``` A field typed by the host's own type parameter does not compile with a `type` constant. On `@prop`, pass a parser that keeps the raw value. On `@attr`, drop `type`: ```ts export class Select extends RadiantElement { @prop({ type: String, transform: { fromAttribute: (v) => (v ?? '') as V } }) selected!: V; @attr({ source: 'data-mode' }) mode?: V; } ``` ### Legacy Decorators With `experimentalDecorators: true`, TypeScript calls a field decorator as `(prototype, key)` and never passes the field type. Radiant supports this mode, but none of the field checks above run in it. Every field in the rejected list above compiles there. The decorators also take no value type from the field, so a callback that uses the value, such as `@bindTo`'s `map` or a `transform`, sees `unknown` unless you pass the type argument. The type argument must be the whole field type, `undefined` included (`@bindTo` on `n?: number`). Under legacy decorators it is still checked against the options, but not compared with the field. ```ts // legacy: error, 'v' is of type 'unknown' @bindTo({ attr: 'data-n', map: (v) => v.toFixed() }) @state n = 0; // legacy and standard @bindTo({ attr: 'data-n', map: (v) => v.toFixed() }) @state n = 0; ``` To get the field checks and drop the type arguments, compile with standard decorators and leave `experimentalDecorators` off. ## Recommended Reading Order 1. Start with [@attr](/docs/decorators/attr), [@state](/docs/decorators/state), and [@signal](/docs/decorators/signal). 2. If the host keeps authored light DOM, read [@bindTo](/docs/decorators/bind-to) for value-to-DOM paint and [@onEvent](/docs/decorators/on-event) for events. 3. If the host renders, read [@onUpdated](/docs/decorators/on-updated) only for procedures that are not a straight DOM copy, and use `this.$` for leaves in `render()`. 4. For custom-element public API, add [@prop](/docs/decorators/prop). 5. For controller discovery, add [@controller](/docs/decorators/controller).