Primitive types

The primitive types are the built-in atomic types every other type is built from: string, number, boolean, bigint, symbol, null, undefined, and void. Assignability is by exact name, with no implicit conversion between them, so a string is never a number until you convert it in real code.

Primitive types are the built-in atomic types every other type is built from: string, number, boolean, bigint, symbol, null, undefined, and void. Each names one kind of value directly.

let name: string = "ada"
let count: number = 3
let ready: boolean = true
let nothing: null = null

If you have used TypeScript or JavaScript, these are the same primitives you already know. void is the type of a value you are meant to ignore, most often the result of a function that returns nothing useful. null and undefined are their own distinct types, each with a single value.

A caveat about executable bodies: bigint, symbol, and undefined all exist as types and are honored in annotations, but the corresponding value forms are not yet accepted inside a runnable function body. A bigint literal (90071992547409910n), a Symbol("tag") call, and even the bare value undefined are each rejected as initializers today, so a binding like let missing: undefined = undefined does not check. Use these three at the type level for now, not as values you construct in a body.

Two catch-all types round out the set but are documented on their own pages, because they behave unlike the rest: the unknown type, the type of a value whose type you do not yet know, and never, the empty type that no value inhabits.

Assignability is by name

Primitives are compared by name identity. A value is assignable to a primitive type only when the target is the same-named primitive. There is no widening or automatic conversion between distinct primitives: a string is not a number, and neither one turns into the other on its own.

let n: number = someString    // error: string is not assignable to number
let s: string = someNumber    // error: number is not assignable to string

This is stricter than the loose coercions JavaScript performs at runtime. In CruftScript, if you want a number from a string you convert it explicitly with real code; the type system will not treat one as the other for you. The check runs at compile time, so a mismatch is caught before the program runs.

Because assignability is by exact name, there is no hierarchy among the primitives to climb. The only types that relate to all of them are the two catch-alls: every primitive is assignable to unknown, and never is assignable to every primitive.

Why it works this way

Name-identity assignability keeps each primitive meaning exactly one thing. When a binding is typed number, you know every value it holds is a number, with no silent promotion from some other primitive that happened to look compatible. That guarantee is what lets the rest of the type system reason precisely: literal types narrow to a single primitive value, unions combine distinct primitives without blurring them, and narrowing checks like typeof x === "string" can hand back an exact primitive type. The cost is that conversions must be written out, which we consider a fair price for knowing what a typed value actually is.

Limitations

  • No implicit conversion between primitives. You cannot assign a string to a number binding, or rely on any automatic coercion. Convert explicitly when you need to cross from one primitive to another.
  • void marks an ignorable result, not a value you build with. Treat it as the return type of something that yields nothing useful, rather than a type to store and read back.