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 3int / 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.5Overflow 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 overflowThe 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 asany, sox.aandx.bwork on anA & Bvalue. - Conditional types:
T extends U ? X : Y. Checked asany. - Mapped types:
{ [K in keyof T]: ... }. Checked asany. - Template literal types:
`on${string}`. Checked asstring. - The
inferkeyword, inside a conditional type. keyofas a type operator. It is checked asstring.typeof xin a type annotation is supported.- Type predicates: a
x is stringreturn type. The function returnsboolean, 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 asany. - Index signatures in a type literal:
{ [key: string]: int }. Checked asany. - Generic constraints:
T extends Something. Not enforced: any argument is accepted. - Type-only imports:
import type { X } from "./x". Thetypekeyword is dropped and the name is imported, but, as in TypeScript,Xis then usable only in type positions. - Static initializer blocks:
static { ... }inside a class. The block never runs;thicket fmtkeeps it. - Type arguments on calls:
first<string>(names)andnew 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); // IntroThe 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.
| Feature | What you get | Use 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 error | Function declarations or arrows |
eval() | Not defined | Nothing: 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*andyielddon't parse. Symbol. Not defined.- The wrapping
ProxyandReflect.new Proxy(target, handler)andReflectare not defined. For wildcard property hooks, extend the built-inProxyclass instead.
Different on purpose
Dateis ThicketScript's own struct, not the JavaScript class. There is nonew Date().Date.now()returns aDate, andDate.nowMs()gives epoch milliseconds.- No prototype chain, no
thisrebinding, no implicit coercion. A method'sthisis 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.