---
title: "User-defined types"
description: "Define your own typed classes with fields and methods, construct them, call methods, and mutate state through this. Model trading state in the shape of the…"
order: 19
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/user-defined-types.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# User-defined types

Define your own typed classes with fields and methods, construct them,
call methods, and mutate state through `this`. Model trading state in the
shape of the problem. A wrun indicator uses an AssemblyScript `class`,
with typed fields, a constructor, and methods, allocated once and reset
in place.

## Introduction

A `class` is a struct you define: a named bundle of typed fields,
optionally with methods that operate on those fields. Instead of tracking
a supply zone as three loose variables (`zoneTop`, `zoneBottom`,
`zoneTouches`), you describe it once as a `Zone` and work with whole
zones.

```text
class Zone {
  top: f64 = NaN;
  bottom: f64 = NaN;
  touches: i32 = 0;
}
```

Each field is declared with a name, a type, and (best practice) an
initializer. Once a class exists, you create instances of it, read and
write their fields, and pass them around like any other value.

## Construct with a constructor

You build an instance with `new`, and a constructor sets the fields from
its arguments:

```text
class Zone {
  top: f64;
  bottom: f64;
  touches: i32 = 0;

  constructor(top: f64, bottom: f64) {
    this.top = top;
    this.bottom = bottom;
  }
}

const z = new Zone(high, low);
const h = z.top - z.bottom;
```

Arguments are positional, so name the parameters well and keep the order
obvious (top before bottom, open before close). A field the class does not
declare cannot be set: `z.label = ...` is a compile error, so a typo is
caught before the file runs. A field left without an initializer or a
constructor assignment starts at zero (or `null` for a reference); give
numeric fields an explicit `NaN` when "not set yet" must be distinguishable
from `0`.

## Add behavior with methods

A class can carry methods. Declare them inside the class, and they read and
mutate the instance through `this`:

```text
class Zone {
  top: f64;
  bottom: f64;
  touches: i32 = 0;

  constructor(top: f64, bottom: f64) {
    this.top = top;
    this.bottom = bottom;
  }

  height(): f64 {
    return this.top - this.bottom;
  }

  registerTouch(price: f64): bool {
    if (price >= this.bottom && price <= this.top) {
      this.touches += 1;
      return true;
    }
    return false;
  }
}
```

Now the data and the logic that maintains it live together. `height()`
derives a value from the fields. `registerTouch(price)` is a self-updating
operation: ask the zone whether the current price touched it, and it
answers while bumping its own `touches` counter. The zone manages its own
state.

```text
const inZone = z.registerTouch(close);   // true or false, and z.touches updates itself
const tall = z.height();
```

This is why classes matter for indicators. A "supply zone that counts its
own touches and retires after the third," an "order block that tracks
whether price mitigated it," a "trailing-stop level that ratchets": each
becomes one class whose methods enforce its rules, instead of bookkeeping
smeared across the whole file. Hold one in a module-level variable to track
one, or in a preallocated `StaticArray<Zone>` to manage a fixed set of
them across bars (`collections.md`).

## Two rules a class must follow here

**Allocate once.** `new Zone(...)` takes memory the module never gives
back, so a class instance is built at module scope or in `onStart()`, never
per bar. When a zone is replaced, mutate the instance you have (a `set(top,
bottom)` method) rather than constructing a new one. A fixed set of zones
is a `StaticArray<Zone>` filled in `onStart()`; a slot that may be empty is a
`StaticArray<Zone | null>` and is tested with `!== null` before use.

**Give it a `reset()`.** A class that carries state across bars gets its
own `reset()` method that puts every field back to its start, so one call
puts the whole zone back instead of three assignments spread through the
file. (The chart replays the forming bar from a snapshot of the whole
module, so nothing of yours has to run for that: [Repainting](repainting.md).)

## A complete typed class

This file declares `Zone` with both methods and a `set()`, keeps one
instance at module scope, re-anchors it to the previous bar's candle body
whenever price leaves it, and lets the methods mutate `touches` through
`this`. Three outputs show the zone's height, its running touch count, and
whether this bar touched it.

```typescript sample=cc-udt-zone
param("max_touches", 3, { min: 1, max: 20, description: "Touches after which the zone retires and re-anchors" });
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("close", ohlcv.close);
output("zone_top", line, overlay, { color: "#7c3aed", width: 1, description: "Top of the tracked zone" });
output("zone_bottom", line, overlay, { color: "#7c3aed", width: 1, description: "Bottom of the tracked zone" });
output("height", line, lower, { color: "#2563eb", description: "Zone height (a method over the fields)" });
output("touches", line, lower, { color: "#16a34a", description: "Touches registered on the current zone" });
output("touch", shape, overlay, { color: "#16a34a", shape_where: "touched", description: "This bar's close touched the zone" });
output("touched", none);

class Zone {
  top: f64 = NaN;
  bottom: f64 = NaN;
  touches: i32 = 0;

  set(top: f64, bottom: f64): void {
    this.top = top > bottom ? top : bottom;
    this.bottom = top > bottom ? bottom : top;
    this.touches = 0;
  }

  height(): f64 {
    return this.top - this.bottom;
  }

  contains(price: f64): bool {
    return price >= this.bottom && price <= this.top;
  }

  registerTouch(price: f64): bool {
    if (this.contains(price)) {
      this.touches += 1;
      return true;
    }
    return false;
  }

  reset(): void {
    this.top = NaN;
    this.bottom = NaN;
    this.touches = 0;
  }
}

const zone = new Zone(); // one instance, allocated once at module scope
let maxTouches: i32 = 3;
let prevOpen: f64 = NaN;
let prevClose: f64 = NaN;

function onStart(): void {
  maxTouches = i32(p_max_touches());
}

function onBar(): void {
  const open = bar.open();
  const close = bar.close();
  // Anchor the zone to the previous bar's body when there is none yet or the current one has retired.
  if (isNaN(zone.top) && !isNaN(prevOpen)) zone.set(prevOpen, prevClose);
  const touched = !isNaN(zone.top) && zone.registerTouch(close);
  if (zone.touches >= maxTouches) zone.set(open, close); // retire: re-anchor to this bar's body
  prevOpen = open;
  prevClose = close;
  if (isNaN(zone.top)) return;
  out_zone_top(zone.top);
  out_zone_bottom(zone.bottom);
  out_height(zone.height());
  out_touches(f64(zone.touches));
  out_touch(close);
  out_touched(touched ? 1.0 : 0.0);
}
```

**What to expect:** `zone.height()` returns the band width,
`registerTouch(close)` reports whether the close fell inside the band and
increments `zone.touches` when it did, and after `max_touches` touches the
zone re-anchors to the current bar's body. The whole zone is one value
carrying its own data and behavior, and `zone.reset()` puts it back in one
call.

## Rules and gotchas

A few constraints the compiler enforces, each at compile time with a
precise line and column.

**Fields are typed.** `top: f64;` is required; an untyped field is a
parse error (`Type expected.`), and assigning an `i32` literal to an `f64`
field needs the point (`0.0`) or a cast.

**Constructors are positional.** There are no named arguments;
`new Zone(top, bottom)` takes the arguments in the constructor's order.
A class without a constructor is built with `new Zone()` and its
initializers.

**`this` only works inside methods.** It refers to the current instance
and is meaningless at module scope or inside a free function.

**Unknown fields are refused.** `z.label` on a class without `label` fails
with `Property 'label' does not exist on type 'src/indicator/Zone'` (the
compiler prints a class by its file path).

**A class is a reference.** Assigning an instance to another variable
aliases it; two zones need two instances, allocated once each.

**Nullable slots need a test.** A `StaticArray<Zone | null>` element is
read into a local and tested with `!== null` before its fields are
touched; using it as a `Zone` without the test fails with `Type
'src/indicator/Zone | null' is not assignable to type 'src/indicator/Zone'`.

Classes are part of the broader type system: `type-system.md` covers how
`f64`, `i32`, `bool`, and `string` fields fit together, and
`collections.md` covers arrays of instances.
