Skip to content

Coming from TypeScript ​

ThicketScript shares TypeScript's syntax, but it is its own language, with its own type checker, compiler and VM. There is no JavaScript underneath. Most of what you write in TypeScript works as it is. This page covers the rest: what works differently, what is accepted and ignored, and what is left out.

The differences that matter ​

Numbers have real types ​

There is no number. Pick int (32-bit signed), uint (32-bit unsigned) or float (64-bit). Types are always lowercase: int, uint, float, string, boolean.

Int(x), Float(x) and their siblings are conversion functions, not types. Int("42") turns a string into an int, and raises if it can't. Writing Int where a type goes is an error that points you to int.

ts
const n: int = Int("42");
const f: float = Float("1.5");
console.log(n + 1, f * 2.0);
text
43 3

int / int truncates ​

Dividing two int values gives an int, rounded toward zero. A float operand, or a float target, makes it float math.

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

Overflow raises an error ​

Integers never wrap and never quietly turn into floats. Going past the range raises a RangeError, so the bug shows up on the line that caused it.

ts
const big: int = 2147483647;
try {
    console.log(big + 1);
} catch (e) {
    console.log(e);
}
text
RangeError: integer overflow

The same goes for packed struct fields: storing a computed 300 in a uint8 raises instead of keeping 44, and a literal 300 there is a compile error.

Equality is strict ​

== and === both compare without coercion. Comparing a string to a number is a checker error, because it can never be true.

Interfaces are nominal ​

A class counts as an interface only when it says implements. Matching the shape is not enough.

Structs are values ​

struct is new. A struct is copied on assignment, carries methods and operators, and can use packed number fields. See Features.

Async has more words ​

series and parallel sit beside async and await. async worker runs a function on its own thread, and async mutex runs one call at a time. See Async, series and parallel.

Accepted and ignored ​

These type-level features parse without error. They have no runtime effect, and thicket fmt prints them as you wrote them. You can paste TypeScript that uses them and keep going.

  • Intersection types: A & B. The type is checked as any, so x.a and x.b work on an A & B value.
  • Conditional types: T extends U ? X : Y. Checked as any.
  • Mapped types: { [K in keyof T]: ... }. Checked as any.
  • Template literal types: `on${string}`. Checked as string.
  • The infer keyword, inside a conditional type.
  • keyof as a type operator. It is checked as string. typeof x in a type annotation is supported.
  • Type predicates: a x is string return type. The function returns boolean, and the call does not narrow.
  • Indexed access types: T[K], such as the value of a mapped type, { [K in keyof T]: T[K] }. Checked as any.
  • Index signatures in a type literal: { [key: string]: int }. Checked as any.
  • Generic constraints: T extends Something. Not enforced: any argument is accepted.
  • Type-only imports: import type { X } from "./x". The type keyword is dropped and the name is imported, but, as in TypeScript, X is then usable only in type positions.
  • Static initializer blocks: static { ... } inside a class. The block never runs; thicket fmt keeps it.
  • Type arguments on calls: first<string>(names) and new Box<int>(...).

Union types are not on this list. They are fully checked: a union with null needs ?. or a guard before you use it, and typeof and is narrow a union inside a branch. abstract is not on it either. Abstract classes and methods are enforced.

Non-null assertions are checked ​

ThicketScript supports TypeScript's postfix ! with the same syntax and typing. It narrows 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

The deliberate difference is run-time behavior. TypeScript erases !, so a wrong assertion carries the null forward until something else fails. ThicketScript checks it: a nullish value raises a TypeError at the ! that names the expression. One more difference: x! = v is an error here (assign to x), where TypeScript accepts it. Use ?? or an explicit guard when absence is normal.

Two TypeScript features look ignorable and are not. Index signatures on an interface are discarded, so reading an undeclared property is an error (Property 'zzz' does not exist): declare the properties, or use Map<string, T>. Declaration merging does not happen: a second interface A { ... } is an error (Duplicate declaration 'A'), so write one declaration.

Map.get can be undefined ​

As in TypeScript, map.get(key) is typed V | undefined, and has(key) does not narrow it. Use ! when the key must exist, ?? when a missing key is normal, or test the result against undefined:

ts
const ages: Map<string, int> = new Map<string, int>();
ages.set("ada", 36);
const known: int = ages.get("ada")!;
const other: int = ages.get("bob") ?? 0;

The bracket read ages["ada"] is typed int | undefined too, like get; ages["ada"] = 1 is checked against int.

Left out ​

These are recognized and rejected with a clear error.

FeatureWhat you getUse instead
Decorators, @sealed"Decorators are not supported"Plain functions or base classes
BigInt, 10n"ThicketScript has no bigint/int64"int, uint or float (exact to 2^53)
Private fields, #x"Private fields are not supported"The private modifier
new Function()A runtime errorFunction declarations or arrows
eval()Not definedNothing: there is no runtime code generation

These have no support at all:

  • Namespaces. namespace Foo { } doesn't parse. import * as ns from "./mod" works.
  • Generators. function* and yield don't parse.
  • Symbol. Not defined.
  • The wrapping Proxy and Reflect. new Proxy(target, handler) and Reflect are not defined. For wildcard property hooks, extend the built-in Proxy class instead.

Different on purpose ​

  • Date is ThicketScript's own struct, not the JavaScript class. There is no new Date(). Date.now() returns a Date, and Date.nowMs() gives epoch milliseconds.
  • No prototype chain, no this rebinding, no implicit coercion. A method's this is its object.

Why leave things out ​

ThicketScript keeps the practical core you use to write a game and drops what adds weight without helping game code. The aim is a language a TypeScript developer can read on day one, running on a small, fast VM.