Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

generic component

component Name<T>(Binding<T> value, T[] rows = T.Members) [where T : enum] { … T.Members … }

A component with type parameters, bound at each call site from the arguments — one component that works over any enum or any row type, instead of a near-identical copy per type.

preview1 example compiled by CIuiauthoringgenerics

Summary#

A component may declare type parameters. They are inferred at each call site from the arguments — there is no constraint to declare and no type to pass — so one Dropdown<TEnum> serves every enum in the app, and one Table<TRow> serves every row type. Inside the body a type parameter is a real type: it types parameters, action parameters and locals, and TEnum.Members reads the members of whichever enum the call site bound.

Signature#

component Name<T>(Binding<T> value) { … }        // T inferred from the bound field's type
component Name<T>(T[] rows) { … }                // T inferred from the collection's element type
component Name<T>(T item) { … }                  // T inferred from the argument's type

component Name<T>(Binding<T> value) where T : enum { … }   // …or state the constraint outright

Name(value: org.Status)                          // the call site says what T is

Description#

The type parameter is inferred at the call site#

A call site already names the type: Dropdown(value: org.Status) can only mean OrganizationStatus. So there is no explicit type argument to pass. A parameter binds a type parameter when it is declared as T (from the argument's own type), T[] (from the collection's element type) or Binding<T> (from the bound field's type). The first parameter that names a given type parameter wins; a later one is checked against that binding.

What the type parameter must BE is normally inferred too, from what the body does with it: a body that reads T.Members has said that T is an enum, and every call site is checked against that. You do not have to write it down — see Declaring the constraint below for the one case where you do.

Declaring the constraint — where T : enum#

Inference works because the compiler can see the body. A component you did not declare in this app — one from a kit or capability — is compiled separately, and its body is not re-read here. So there is nothing to infer from, and a call to it is refused rather than guessed at:

'Dropdown' is generic, but nothing here says what 'TEnum' has to be…

Guessing "unconstrained" would be worse than refusing: the body would ask for the members of something that is not an enum, get none, and render an empty control that looks live and does nothing.

A declared constraint fixes this, because it lives on the declaration rather than in the body, and the declaration is what crosses into your app:

component Dropdown<TEnum>(Binding<TEnum> value) where TEnum : enum {
  render {
    foreach (var m in TEnum.Members) { Text(m.Label); }
  }
}

enum is the only constraint kind. Write where once per constrained parameter, after the parameter list: component Pair<A, B>(A a, B b) where A : enum where B : enum.

Rule of thumb: if the component lives in your own app, you never need where — inference covers it. Declare it when you ship a component for other apps to call and they cannot see its body.

A declared constraint is unconditional, which is exactly what you want for a component that is always over an enum — and exactly what you must not write for one that is only sometimes over an enum. See the next section.

A constraint that only applies when you fall back#

A fallback — a parameter's default value, or a slot's default body — is what the component does instead of something you may supply. So a demand it raises binds only the call sites that actually reach it:

component Dropdown<T>(Binding<T> value, T[] options = T.Members) {
  render {
    foreach (var o in options) { Slot(o) { Text(o.Label); } }
  }
}

Dropdown(value: order.Status)                                       // T = OrderStatus — falls back, so T must be an enum
Dropdown(value: project.Lead, options: people) { p => Text(p.Name); }   // T = User — supplies both, so it need not be

Both defaults above say the same thing: if you name no options, and no template, this is an enum picker. Neither says T is always an enum — and reading them that way would refuse the second call, over a constraint neither of its arguments touches.

The two are suppressed by different things, so they narrow separately:

FallbackApplies to a call site that…
T[] options = T.Memberspasses no options: argument
Slot(o) { … }'s default bodywrites no template for that slot

Anything the body does outside a fallback still binds every call site, however much it supplies. And a slot whose name is computed (Slot(col.Key, row)) cannot be matched against a call site's fills, so a demand from its default body stays unconditional — an escaped demand would render the empty, live-looking control this check exists to refuse.

This is why the kit's Dropdown declares no where clause. It is one component over an enum or over any rows, and its enum-ness lives entirely in those two fallbacks; a declared where T : enum would re-refuse every collection-backed call.

What a type parameter can do inside the body#

It behaves as a type, not as an escape hatch — the body is fully checked:

WrittenMeans
Binding<T> valuea two-way binding over a T, exactly like Binding<OrgRole>
action Choose(T m)an action taking a T
value = massignment, when both are the same T
m == valueequality, when both are the same T (two different type parameters are not comparable)
T.Membersevery member of the enum T was bound to (see below)
m.Label / m.Description / m.Namethe enum's words ([Label], [Icon], [Tone] — what a human reads), read off a T-typed value
Icon(m)the glyph the member declares with [Icon(…)]
passing m where a tone enum is expectedthe tone the member declares with [Tone(…)]

Anything else off the bare name — T.Anything — is a compile error: a type parameter has no members of its own.

T.Members and when it is an enum#

Reading T.Members (or a word off a T-typed value) is what makes T an enum — that use IS the constraint. Every call site is then checked, and binding something that is not an enum is a compile error naming the component, the parameter and the type it got:

'Dropdown' reads TEnum.Members, so 'TEnum' has to be an ENUM — but 'value' here is string.

A type parameter the body never uses as an enum — Table<TRow>(TRow[] rows) — binds to anything.

Decoration comes from the model, not from the component#

Icon(m) renders the [Icon(…)] the member declares, and passing m where a tone enum is expected renders its [Tone(…)] — the same two spellings that already work on a concrete enum value, now reaching a type parameter. So a generic component decorates every enum without naming a single member.

Both carry a completeness demand to the call site, because a half-decorated enum would render a blank glyph or an unstyled row — something that looks deliberate and carries none of the meaning the model declared. Binding an enum whose members do not all declare what the body renders is a compile error naming the members that don't:

'Dropdown' renders each member's [Icon(…)], so EVERY 'Bare' member needs one — 'NoIcon' does not.

This is the same check the concrete spelling makes; it simply moves to the call site, which is the only place that knows which enum is in play.

The list is the same one <Enum>.Members gives for a concrete enum, in declaration order, and the values are the same, so m == value and m.Label behave identically either way. The difference is when: a concrete Status.Members is expanded by the compiler, while T.Members is read at render time from the enum this particular instance was bound to. That is what lets one component body serve two call sites over different enums on the same page.

One body, many call sites#

A generic component is compiled once. The type each call site inferred travels to that instance, so two instances of one component can list two different enums side by side. Nothing is duplicated per type: adding a fifth enum-backed picker to an app adds a call, not a component.

Examples#

One dropdown, two enums, on one page. Both instances are the same component; each lists its own enum's members and writes the chosen one back through its binding.

The Dropdown below is THIS PAGE'S, declared above — not the kit's. The name is deliberate (a generic control is what the kit ships one of), and the shape is cut down to the one thing being taught: type inference. The kit's own control takes a required caption first — Dropdown("Status", value: order.Status) — see Osyrin.Ui (the UI kit).

enum Tone { Neutral, Accent }

enum Status {
  /// Open for business
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Active")] Active,
  /// Temporarily closed
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Suspended")] Suspended,
}

enum Role {
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Owner")] Owner,
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Member")] Member,
}

// One option row. `tone` is an ordinary enum-typed param — passing a member of ANOTHER enum coerces to the tone that
// member declares, which is what lets the generic body above decorate without knowing the enum.
[Composable] component Option(string label, Tone tone) {
  // Literal colours here only because this example declares no `theme` — a real app uses its declared tokens.
  variants { base { Px = 1; } tone { Neutral { Bg = "#f6f6f7"; } Accent { Bg = "#e8f0fe"; } } }
  render { Row(gap: 1) { Slot; Text(label); } }
}

// `[Composable]` for the same reason `Option` above has it: the page below is PUBLIC, and an anonymous visitor can
// only load a component that says it carries no auth identity of its own ([[Composable] — presentational components in public pages](/reference/ui/composable/)). Its data reads, if it
// had any, would still be gated.
[Composable]
component Dropdown<TEnum>(Binding<TEnum> value) {
  bool open = false;
  action Toggle() { open = !open; }
  action Choose(TEnum m) { value = m; open = false; }

  render {
    Box(position: Position.Relative) {
      Pressable(onClick: Toggle) { Text(value.Label); }
      if (open) {
        Stack(gap: 0) {
          foreach (var m in TEnum.Members) {
            // Every option's glyph and tint comes from the member's own declaration — nothing here names one.
            Pressable(onClick: () => Choose(m)) { Option(m.Label, m) { Icon(m, size: 14); } }
          }
        }
      }
    }
  }
}

[Page("/settings")]
[AllowAnonymous]
component SettingsPage() {
  Status status = Status.Active;
  Role role = Role.Member;
  render {
    Stack(gap: 2) {
      Dropdown(value: status);
      Dropdown(value: role);
    }
  }
}

A type parameter that is never used as an enum takes any type — this is the row-type case, and it is checked the same way (the datum in the template binds to T).

component Table<TRow>(TRow[] rows) {
  render { Stack(gap: 1) { foreach (var r in rows) { Text("row"); } } }
}

Table(rows: expenses)     // TRow = Expense

See also#

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

[Label], [Icon], [Tone] — what a human reads

An enum member stores a compact value but shows a human-readable label. [Label("…")] gives a member its label…

enum

A fixed set of named values, used as a member type. Stored as a number by default, or as the member's own name with…