---
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: 17
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. kScript (legacy) v3 called these `type` structs with
`.new(...)` named-field construction; an Indicator uses an AssemblyScript
`class`, with typed fields, a constructor, and the same 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 `init()`, 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 `init()`; a slot that may be empty is a
`StaticArray<Zone | null>` and is tested with `!== null` before use.

**Give it a `reset()`.** The host replays the forming bar into a snapshot
of the module, and the module's `reset()` must put every piece of state
back. A class that carries state across bars gets its own `reset()`
method, and the module's `reset()` calls it. A field it forgets is a
stale value the replay reads (`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
import { input, line, lower, none, ohlcv, output, overlay, param, shape } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_height, out_touch, out_touched, out_touches, out_zone_bottom, out_zone_top } from "./gen/outputs";
import { p_max_touches } from "./gen/params";

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;
let close: f64 = NaN;
let touched: bool = false;

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

export function state(): i32 {
  const open = in_open();
  close = in_close();
  in_high();
  in_low();
  // 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);
  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;
  return isNaN(zone.top) ? 0 : 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  zone.reset();
  prevOpen = NaN;
  prevClose = NaN;
  close = NaN;
  touched = false;
}
```

**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()` is one call in the
module's `reset()`.

## 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 is no `Zone.new(top=..., bottom=...)`;
`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.
