Skip to content

Types ​

ThicketScript checks types before your program runs. The checker catches mistakes early, and the IDE uses the same information for autocomplete.

The basic types ​

TypeWhat it holdsExample
int32-bit signed integerlet lives: int = 3;
uint32-bit unsigned integerlet color: uint = 0xFF8800FF;
float64-bit floating pointlet speed: float = 2.5;
stringTextlet name: string = "Ada";
booleantrue or falselet ready: boolean = false;
voidNo value, for function returnsfunction log(): void {}
null, undefinedNo valuelet target: Enemy | null = null;
anyTurns checking off for one valueUse sparingly.

There is no number type and no bool. Pick int, uint or float, and write boolean.

Integers are integers ​

Arithmetic on two int values stays an int. That includes division, which truncates toward zero:

ts
const a: int = 7;
const b: int = 2;
console.log(a / b);      // 3
console.log(-9 / 4);     // -2

const half: float = a / b;
console.log(half);       // 3.5

A float target, like half above, makes the whole expression float math. A float operand does the same.

Integers never wrap silently. Going past the range raises a RangeError:

ts
const big: int = 2147483647;
console.log(big + 1);    // RangeError: integer overflow

Values above 0x7FFFFFFF, like most RGBA colors, need uint.

Number literals ​

ts
const hex: int = 0xFF;
const binary: int = 0b1010;
const octal: int = 0o77;
const ratio: float = 0.75;
console.log(hex, binary, octal, ratio);

Strings ​

Strings use double quotes, single quotes or backticks. Backticks allow ${} interpolation:

ts
const name: string = "Ada";
const level: int = 4;
console.log(`${name} reached level ${level}`);
console.log(name + " has " + level + " gems");
console.log(name.toUpperCase(), name.length);

"a" + 1 is "a1", as in TypeScript.

Unions and null ​

A union lets a value be one of several types. Adding null makes a value optional:

ts
function greet(name: string | null): string {
    if (name != null) {
        return "Hello, " + name;
    }
    return "Hello, stranger";
}

console.log(greet("Ada"));
console.log(greet(null));

Inside the if, the checker knows name is a string. That is called narrowing. It works with != null, truthiness checks, is, instanceof and early returns.

x != null catches both null and undefined, and narrows both away from string | null | undefined; x == null matches both. Strict x !== null removes only null, and x !== undefined only undefined. The ?. and ?? operators work as in TypeScript:

ts
class Weapon {
    damage: int = 5;
}

class Hero {
    weapon: Weapon | null = null;
}

const hero: Hero = new Hero();
const damage: int = hero.weapon?.damage ?? 1;
console.log(damage);    // 1

Use postfix ! when a value must exist but a guard is impractical. It narrows the expression from T | null | undefined to T:

ts
function findTitle(id: int): string | null {
    return id == 1 ? "Intro" : null;
}

const title: string = findTitle(1)!;
console.log(title); // Intro

This differs from TypeScript: ThicketScript checks ! at run time. A null or undefined value raises a TypeError at the assertion, and the message names the expression (non-null assertion failed for 'findTitle()': value is null).

Equality is strict ​

== and === mean the same thing. Neither converts types. 0 == "" is false. The one exception is null == undefined, which is true, so x != null is the usual null check.

Truthiness ​

null, undefined, false, 0 and "" are falsy. Everything else is truthy, including empty arrays and empty objects.

Type aliases and literal types ​

ts
type Direction = "up" | "down" | "left" | "right";
type EntityId = int;

let facing: Direction = "up";
const id: EntityId = 42;
console.log(facing, id);

Aliases exist only for the checker. They produce no code.

Arrays, tuples, maps and sets ​

ts
const scores: Array<int> = [10, 20, 30];
scores.push(40);
console.log(scores.length, scores[0]);

const doubled: Array<int> = scores.map((s: int): int => s * 2);
console.log(doubled.join(", "));

const pair: [string, int] = ["level", 3];
console.log(pair[0], pair[1]);

const hp: Map<string, int> = new Map<string, int>();
hp.set("slime", 12);
hp.set("bat", 6);
console.log(hp.get("slime"), hp.has("bat"), hp.size);

// get answers `int | undefined`: a missing key is undefined.
const slime: int = hp.get("slime")!;       // ! : the key must exist
const dragon: int = hp.get("dragon") ?? 0; // ?? : a missing key is normal
console.log(slime, dragon);

for (const [name, value] of hp) {
    console.log(name, value);
}

const seen: Set<string> = new Set<string>();
seen.add("cave");
console.log(seen.has("cave"));

An array's element type is enforced. Pushing a string into an Array<int> is an error.

Arrays come with extras beyond the JavaScript set, such as first(), last(), isEmpty(), unique(), shuffle(), sum() and avg().

Checking a type at run time: is ​

is tests a value and narrows it:

ts
function describe(value: int | string): string {
    if (value is int) {
        return "a number: " + value;
    }
    return "some text: " + value;
}

console.log(describe(5));
console.log(describe("hi"));

is works with the basic types, the built-in object types (Array, Map, Set and others), your classes and structs.

Converting and casting: as ​

Between basic types, as converts:

ts
const text: string = 42 as string;       // "42"
const whole: int = 3.9 as int;           // 3, truncates toward zero
const parsed: int = "120" as int;        // 120
console.log(text, whole, parsed);

A string that is not a number converts to NaN. Check it with isNaN(). When you would rather get an error, use Int(text), which throws, or tryInt(text), which returns null.

Between classes, as asserts. If the value is not that class, it throws a TypeError instead of carrying on with the wrong object. Use is first when you are not sure.

Structs ​

A struct is a value type with a fixed memory layout. Struct fields can use compact storage types such as uint8, int16 and float32:

ts
struct Pixel {
    r: uint8;
    g: uint8;
    b: uint8;
    a: uint8;
}

const p: Pixel = Pixel{ r: 255, g: 128, b: 0, a: 255 };
const red: uint = p.r;
console.log(red);

Reading a packed field gives you an ordinary uint, int or float. Writing checks the range. Storing a wider value without as is a warning (PF072), but when every input already fits the field, as in Pixel{ r: (a.r + b.r) / 2, ... } or Vec{ x: a.x + b.x } with float32 fields, there is no warning. A literal that cannot fit, such as r: 300, is a compile error. The compact types are only for struct fields and similar storage: variables, parameters and class fields use int, uint and float.

When dump() renders a struct, a float32 field uses the shortest decimal that round-trips at single precision. A field read on its own is an ordinary float, so format that value explicitly when it needs a fixed display width.