---
title: "Type system"
description: "A comprehensive guide to the Indicator type system: static typing, how types are inferred from literals, explicit casts, the per-bar value that replaces a…"
order: 14
section: "core-concepts"
---

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

# Type system

A comprehensive guide to the Indicator type system: static typing, how
types are inferred from literals, explicit casts, the per-bar value that
replaces a series, and the messages the compiler prints when a type does
not fit. kScript (legacy) used a hybrid system, static analysis over a
dynamic runtime with one `number` type and an implicit series type. An
Indicator is AssemblyScript: every value has a fixed width, nothing
converts implicitly, and there is no `any`.

## Introduction

The compiler type-checks the whole file before it runs. There are no
implicit conversions between numeric widths, no coercion between strings
and numbers, and no union types. That strictness is the point: a mismatch
is a compile error with a line and column, never a wrong number on bar
1,400.

At its core the system has two categories of values: **primitives**
(`f64`, `i32`, `bool`, `string`) and **references** (arrays, maps, class
instances). What it does not have is a series type. kScript's
domain-specific strength was `timeseries`, a value with a whole history
behind it; an Indicator's central idea is the opposite: one value per bar,
read through a function, with history kept explicitly. This page covers
declarations and inference, the primitives, the per-bar model, collections,
conversion rules, and a complete example.

## Variable declarations and type inference

`let` and `const` declare variables, as in TypeScript. The type comes from
the annotation if there is one, otherwise from the initializer, and the
rule to remember is the literal rule: **a whole-number literal is an
`i32`, a literal with a decimal point is an `f64`.**

```text
let price = 45000.5;     // f64
let count = 0;           // i32, because the literal has no point
let flag = true;         // bool
let name = "BTCUSDT";    // string

let value: f64 = 0;      // annotated: f64 (the literal widens)
let cursor: i32 = 0;     // annotated: i32
```

Once inferred, the type is fixed. `let count = 0; count = 2.5;` is refused
with `Conversion from type 'f64' to 'i32' requires an explicit cast`,
which is also the message you get when a param (always `f64`) meets a
class constructor (always `i32`): write `new Sma(i32(p_period()))`.

`const` fixes the binding, not the contents: a `const window = new
StaticArray<f64>(50)` is filled and overwritten freely; only `window =
...` is refused. Module-level TA objects are `let`, because `init()`
rebuilds them at the chosen period.

The three lifetimes (a local, a module-level variable, a value with
history) are in `core-variables.md`; this page is about what the values
are.

## Primitive types

**`f64`** is the 64-bit float and the type of everything that crosses the
host boundary. Arithmetic on two `f64` values is an `f64`; dividing by zero
gives `Infinity` or `NaN`, never an error, so guard denominators. `NaN` is
the missing value: `isNaN(x)` tests it, `x == x` is false when `x` is
`NaN`, and every comparison with `NaN` is false.

**`i32`** is the 32-bit integer: periods, counters, cursors, loop bounds,
the `state()` return, `Cross.update()`'s answer. Integer division truncates
(`7 / 2` is `3`). `i64` exists for epoch-second math (`time-and-sessions.md`).

**`bool`** comes from comparisons and logical operators. A bare number in an
`if` compiles (nonzero is true), but write the comparison out: `x > 0.0`
says what you mean, and `!isNaN(x)` is the only reliable test for a missing
value.

**`string`** is immutable text with `length`, `charCodeAt`, `indexOf`,
`startsWith`, `+` between strings, and `==` / `<` by code unit. Strings stay
inside the module and reach the chart only through a declared string slot;
building one allocates, so per-bar text goes through the line builder
(`sb_clear`, `sb_text`, `sb_f64`). There is no number-to-string coercion:
`"close " + close` is refused with `Type 'f64' is not assignable to type
'String'`; write `close.toString()`.

## The per-bar value: what replaces `TimeSeries`

A kScript `timeseries` was a complete history aligned to bars; the engine
tracked it for you and `[n]` looked backward. An Indicator input is a
function: `in_close()` returns this bar's close, and only in `state()`. The
host calls `state()` once per bar, oldest first, so a line you write once
still produces a value at every point in time, but the past is not
addressable.

What holds history:

| Need | Form |
| --- | --- |
| `close[1]` | a module-level `prevClose` assigned at the end of `state()` |
| `sma(close, 20)`, `rsi(close, 14)` | `new Sma(20)`, `new Rsi(14)`: the class keeps the window, `update(x)` folds one value |
| `highest(high, 20)`, `close[5]` | a `StaticArray<f64>` ring buffer of 20 with a cursor |
| `cum(delta)` | a module-level accumulator |

Immutability is built in (an input has no setter), and module scope is
where history lives: anything that must survive a call is declared there,
because a local is gone when `state()` returns.

### OHLCV data structure

A candle is not one value with six columns. Declare one input per field
you read, and read each through its own accessor:

```text
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("close", ohlcv.close);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);   // the timestamp, epoch seconds
```

The first `input` is the primary: it defines the grid (market and
interval) every other input aligns to. There is no `priceIndex`: a class
takes whichever number you hand it, so `sma.update(in_close())` and
`sma.update((in_high() + in_low()) / 2.0)` are the same call over
different sources.

## Arrays and collections

Arrays are typed and homogeneous. `StaticArray<f64>` is a fixed-size
block allocated once; `Array<f64>` grows with `push`; `f64[]` and
`string[]` literals work; `Map<K, V>` is a key-value store. The element
type is fixed at declaration and enforced: pushing a string onto an
`Array<f64>` is refused with `Type 'String' is not assignable to type
'f64'`. Arrays are references: passing one to a function passes the same
storage, so a function that writes into it writes into yours. The
allocation rule applies to all of them: build them in `init()` or at
module scope, never per bar (`collections.md`).

```text
const periods: i32[] = [10, 20, 50];
const window = new StaticArray<f64>(200);
const weights = new Map<string, f64>();
```

## Data sources

kScript offered a direct function per feed and the universal `source("type",
...)`. An Indicator has one form, `input(name, source.field, options)`,
where `source` is a namespace from `./sdk/declare` and `field` one of its
members; every input is an `f64`, and the source decides what the number
means. The catalog, the `missing` policy for sparse feeds, and the named
refusals for feeds a host cannot fetch are in `data-sources.md`.

## Type conversion and casting

Nothing converts on its own. The rules:

- **`f64` to `i32`:** `i32(x)` truncates toward zero. Periods, cursors,
  and loop bounds derived from a param need it.
- **`i32` to `f64`:** `f64(n)`. Writing a counter to an output needs it,
  and so does mixing an `i32` into float arithmetic.
- **`bool` to a number:** a ternary, `flag ? 1.0 : 0.0`.
- **A number to a string:** `.toString()`, and only for module-internal
  text.
- **Never implicit:** `f64 * i32` is refused; cast one side.

```text
const period = i32(p_period());          // f64 to i32
out_count(f64(hits));                    // i32 to f64
out_is_long(fast > slow ? 1.0 : 0.0);    // bool to f64
const label = "n=" + hits.toString();    // i32 to string, inside the module
```

**Type mismatch errors** name the rule. The ones you will meet:

| Message | Cause | Fix |
| --- | --- | --- |
| `AS200: Conversion from type 'f64' to 'i32' requires an explicit cast.` | a param or float where an integer goes | `i32(...)` |
| `TS2322: Type 'bool' is not assignable to type 'i32'.` | a comparator returning a comparison | return `a > b ? 1 : a < b ? -1 : 0` |
| `TS2322: Type 'String' is not assignable to type 'f64'.` | a string pushed onto a numeric array | keep the array homogeneous |
| `TS2329: Index signature is missing in type 'f64'.` | `close[1]` on a number | keep the previous value in a variable |
| `TS1110: Type expected.` | a function without a return type | annotate: `function f(x: f64): f64` |
| `AS100: Not implemented: Closures` | an arrow function reading a local of the enclosing function | read module-level state instead (`lambdas-and-reducers.md`) |
| `AS100: Not implemented: Iterators` | `for (const x of xs)` | an index loop |

## Best practices

Annotate every module-level `let` and every function signature (the
annotation is documentation, and it stops the literal rule from making a
float an integer); feed inputs straight into the classes that keep the
windows; keep params, inputs, and outputs at the top as top-level
statements, state under them, the four functions last.

## Complete example: multi-source momentum

The kScript type-system page closed with a momentum indicator over three
sources. The port reads the same three: the close for an RSI, the funding
rate averaged over 24 bars, and liquidations (sparse, so `missing:
"zero"` makes a quiet bar a `0`). The extreme condition is a data-only
gate and a mark drawn only where it holds.

```typescript
import { funding, input, line, liquidations, lower, none, ohlcv, output, param, shape } from "./sdk/declare";
import { in_close, in_funding, in_liqs } from "./gen/inputs";
import { emitRow, out_extreme, out_extreme_rsi, out_is_extreme, out_rsi } from "./gen/outputs";
import { p_extreme_rsi, p_funding_threshold, p_rsi_period } from "./gen/params";
import { Rsi, Sma } from "./sdk/ta";

// Configuration: the RSI length, the RSI level, and the funding level.
param("rsi_period", 14, { min: 2, max: 100, description: "RSI period" });
param("extreme_rsi", 30, { min: 1, max: 99, description: "RSI at or below this is oversold" });
param("funding_threshold", 0.01, { min: 0, max: 100, description: "Funding below minus this means shorts are paying, in the feed's own units" });
// Data sources: price, funding, liquidations.
input("close", ohlcv.close);
input("funding", funding.rate_close, { description: "Funding rate at the bar's close" });
input("liqs", liquidations.liquidations, { missing: "zero", description: "Liquidation volume, 0 on quiet bars" });
// Visualization: the RSI and its level in one pane, a mark only on extreme bars.
output("rsi", line, lower, { color: "#2962FF", width: 2, description: "Relative Strength Index" });
output("extreme_rsi", line, lower, { color: "#FF6B35", width: 1, description: "Extreme RSI level" });
output("extreme", shape, lower, { color: "#00BA88", shape_where: "is_extreme", description: "Oversold, shorts paying, liquidations printing" });
output("is_extreme", none, lower);

let rsi = new Rsi(14);
let avgFunding = new Sma(24); // 24 bars of funding, one day on a 1h chart
let liqAvg = new Sma(6); // six bars of liquidation volume
let extremeLevel: f64 = 30.0;
let fundingThreshold: f64 = 0.01;
let rsiValue: f64 = NaN;
let extreme: bool = false;

export function init(): void {
  rsi = new Rsi(i32(p_rsi_period()));
  extremeLevel = p_extreme_rsi();
  fundingThreshold = p_funding_threshold();
}

export function state(): i32 {
  rsiValue = rsi.update(in_close()); // f64 in, f64 out
  const fundingAvg = avgFunding.update(in_funding());
  const liqRecent = liqAvg.update(in_liqs());
  // Three bools, combined; NaN on any side makes its comparison false, so a warming source never fires.
  const oversold: bool = rsiValue <= extremeLevel;
  const shortsPaying: bool = fundingAvg < -fundingThreshold;
  const liquidating: bool = liqRecent > 0.0;
  extreme = oversold && shortsPaying && liquidating;
  return isNaN(rsiValue) ? 0 : 1;
}

export function finalize(): void {
  out_rsi(rsiValue);
  out_extreme_rsi(extremeLevel);
  out_extreme(rsiValue); // the mark sits on the RSI line
  out_is_extreme(extreme ? 1.0 : 0.0); // bool to f64
  emitRow();
}

export function reset(): void {
  rsi.reset();
  avgFunding.reset();
  liqAvg.reset();
  rsiValue = NaN;
  extreme = false;
}
```

Watch the types flow: three `f64` inputs feed three classes that return
`f64`, those numbers feed comparisons that return `bool`, the bools combine
into one `bool`, and the ternary turns it into the `f64` gate output that
the `shape` output reads through `shape_where`. Nothing converts on its
own, and every step is checked before the first bar runs.
