> ## Documentation Index
> Fetch the complete documentation index at: https://flux-docs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Composite Types

> Optional, Array and Struct: wrapping other types into structured payloads.

```luau theme={null}
local T = Flux.Types
```

The three composites take other `Type`s as arguments and nest freely, so a schema can
describe structured data without falling back to the dynamic encoder.

## Optional

<ResponseField name="T.Optional(t: Type<T>)" type="Type<T?>">
  Writes a one-byte presence flag, then the inner value only if it is not `nil`.
</ResponseField>

<ParamField path="t" type="Type<T>" required>
  The inner type, used only when the value is present.
</ParamField>

```luau theme={null}
local Target = Flux.Server("Target", {
	T.Optional(T.Instance()),
	T.Optional(T.Vector3()),
})

Target:Fire(player, enemy, enemy.Position)
Target:Fire(player, nil, nil)  -- no target: 2 bytes total
```

<Warning>
  This is the only way to send `nil` under a schema. The plain constructors pass straight
  through to `buffer.write*`, which errors on `nil` with an unhelpful message:

  ```luau theme={null}
  local E = Flux.Server("E", { T.UInt8() })
  E:FireAll(nil)  -- ERRORS inside buffer.writeu8
  ```

  Anything that can be absent, such as an optional target, an unset preference, or a value
  still loading, has to be wrapped.
</Warning>

### Size

| Value   | Bytes             |
| ------- | ----------------- |
| `nil`   | 1                 |
| present | 1 plus inner size |

<Info>
  A `nil` costs one byte, which is the same as the dynamic encoder's `TYPE_NIL` tag. The flag
  is only overhead when the value is usually present, so do not wrap fields that are never
  `nil`.
</Info>

***

## Array

<ResponseField name="T.Array(t: Type<T>, maxLen: number?)" type="Type<{T}>">
  A varint element count followed by each element encoded with `t`.
</ResponseField>

<ParamField path="t" type="Type<T>" required>
  The element type. Every element has to be encodable by it, because arrays are homogeneous.
</ParamField>

<ParamField path="maxLen" type="number">
  Optional maximum element count, asserted on write.
</ParamField>

```luau theme={null}
local Leaderboard = Flux.Server("Leaderboard", {
	T.Array(T.String(20)),      -- names
	T.Array(T.UInt32(), 100),   -- scores, at most 100
})

Leaderboard:FireAll(names, scores)
```

`Read` preallocates with `table.create(n)`, so decoding a large array does not repeatedly
resize.

<Note>
  Arrays are homogeneous and dense. `T.Array(T.UInt8())` over `{ 1, "two", 3 }` errors on the
  second element, and a sparse table like `{ [1] = "a", [3] = "c" }` has `#v == 1`, so only the
  first element is written and the rest is silently lost. Use `T.Array(T.Any())` for mixed
  contents, or a `T.Struct()` if the shape is actually fixed.
</Note>

<Warning>
  As with `T.String`, `maxLen` is checked on write only, because `Read` ignores it. It raises
  `array exceeds max length` at your call site rather than dropping the message, and it provides
  no protection against an oversized array from a tampered client. Validate inbound lengths in
  your server listener.
</Warning>

***

## Struct

<ResponseField name="T.Struct(fields)" type="Type<table>">
  Encodes named fields in declared order. No keys are transmitted, because the layout carries
  them.
</ResponseField>

<ParamField path="fields" type="{ { string | Type } }" required>
  An array of `{ name, type }` pairs. The order is part of the wire format.
</ParamField>

```luau theme={null}
local Spawn = Flux.Server("Spawn", {
	T.Struct({
		{ "Position", T.Vector3() },
		{ "Health", T.UInt16() },
		{ "Team", T.Enum(Enum.TeamColor) },
		{ "Weapon", T.Optional(T.String(32)) },
	}),
})

Spawn:FireAll({
	Position = Vector3.new(0, 10, 0),
	Health = 100,
	Team = Enum.TeamColor.Bright_blue,
	Weapon = "rifle",
})
```

<Info>
  This is the most byte-efficient way to send a table. Compare the payload above:

  | Encoding             | Bytes                                        |
  | -------------------- | -------------------------------------------- |
  | Dynamic `TYPE_TABLE` | about 70, because every key name is a string |
  | `T.Struct`           | 18, with no keys at all                      |

  String keys dominate dynamically encoded dictionaries, and a struct removes them entirely.
</Info>

<Warning>
  The declared field order has to match on both sides, since it determines the byte sequence.
  Reordering the pairs in one place and not the other silently misreads the packet: reads land
  on the wrong offsets and every subsequent value in the packet is corrupted.

  Add new fields at the end, and treat the field list as a versioned wire contract.
</Warning>

<Note>
  Only declared fields are transmitted. Extra keys on the table are ignored, and a missing key
  is read as `nil` and passed to the field's `Write`, which errors for any non-optional type.
  Wrap fields that may be absent in `T.Optional`.
</Note>

<Info>
  The type annotation is `{{ string } | Type}`, which is loose, so Luau will not check that your
  pairs are well formed. A typo like `{ T.UInt16(), "Health" }`, with the order reversed, passes
  analysis and fails at runtime with a `nil` index error. Write the name first.
</Info>

***

## Nesting

Composites take any `Type`, including each other.

```luau theme={null}
local Squad = Flux.Server("Squad", {
	T.Array(T.Struct({
		{ "Player", T.Instance() },
		{ "Loadout", T.Array(T.String(24), 8) },
		{ "Captain", T.Bool() },
		{ "Marker", T.Optional(T.Vector3()) },
	}), 8),
})

Squad:FireAll({
	{ Player = p1, Loadout = { "rifle", "medkit" }, Captain = true,  Marker = Vector3.new(0, 0, 0) },
	{ Player = p2, Loadout = { "shotgun" },         Captain = false, Marker = nil },
})
```

<Tip>
  Extract nested types into named locals when they repeat. Each constructor call allocates a
  fresh table, so reusing one is both clearer and cheaper:

  ```luau theme={null}
  local MemberType = T.Struct({
      { "Player", T.Instance() },
      { "Captain", T.Bool() },
  })

  local Squad = Flux.Server("Squad", { T.Array(MemberType, 8) })
  local Roster = Flux.Server("Roster", { T.Array(MemberType, 32) })
  ```

  A `Type` is immutable in practice, being just two closures, so sharing one across schemas is
  safe.
</Tip>

## Choosing a composite

| Shape                            | Use                 |
| -------------------------------- | ------------------- |
| May be `nil`                     | `T.Optional(inner)` |
| Homogeneous list, varying length | `T.Array(inner)`    |
| Fixed set of named fields        | `T.Struct({ ... })` |
| Heterogeneous list               | `T.Array(T.Any())`  |
| Genuinely unknown shape          | `T.Any()`           |

<Warning>
  Composites are structural, not recursive. There is no way to declare a type that contains
  itself, so a tree or linked structure of arbitrary depth cannot be described by a schema. Use
  `T.Any()` for those, and remember that
  [circular references overflow the stack](/concepts/serialization#tables).
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Schemas guide" icon="ruler" href="/guides/schemas">
    When schemas are worth declaring at all.
  </Card>

  <Card title="Custom types" icon="wrench" href="/api/types#custom-types">
    Writing your own `Write` and `Read` pair.
  </Card>
</CardGroup>
