Gizlang for TypeScript programmers
Gizlang uses the spelling of TypeScript. This page shows only the differences. TypeScript
features that are not on this page are compile errors. They never get a different meaning.
giz check FILE gives the line, the rule and usually the fix.
This page is about .giz files: every form compiles to static native code. A .ts, .mts or .cts file keeps its
TypeScript meaning on the JavaScript compatibility engine, with the types erased (COMPILER: JavaScript entry).
Types. i64 and f64 (number is f64), u64, bool, string, bytes, Vec<T>
(T[]). A string is UTF-8. Its .length and its indexes count bytes. There are no implicit
conversions. Use i64(x), f64(x) and string(x) (a number or bool as text). Integer /
truncates. An integer literal is an i64: a let sum = 0 that later adds a number is let sum = 0.0 or
let sum: number = 0. a ** b (and **=) is a.pow(b): libm pow on f64, exact on i64 (b >= 0, overflow is a fault).
Overflow and bad indexes stop the program: they are faults, not values. === compares content: scalars, strings, and
the fields of records, unions and Vecs. === on class instances (Ref values) compares identity. Records with hidden fields, views and functions have no ===.
Records and unions are values. Assignment copies them. They are never shared: where TypeScript would show a change of a copy through another name (const b = a; b.x = 1;, or m = a; m.x = 1; with a read after), the compiler refuses it and names a class, which is shared. An operator on a
record calls its method of that name: a + b is a.add(b) (- * / ** < <= > >= -x: sub mul div pow lt le gt ge neg).
type Line = { sku: string; qty: i64; };
type Status = | { kind: "open"; } | { kind: "paid"; amount: i64; };
function main(): void {
const s = Status.paid(5); // or an object literal with an expected type
const line: Line = { sku: "A1", qty: 1 };
const l2 = { ...line, qty: 2 }; // spread a value
const xs = [1, 2];
const ys = [3];
const all = [0, ...xs, 9, ...ys]; // join vectors in order: [0].concat(xs).concat([9]).concat(ys)
print(`${s.kind === "paid"} ${l2.qty} ${all.length}`);
}A let local changes in place at any path: o.lines[i].qty = 3; o.lines.push(l);. A
function changes the value of its caller only through a mut parameter:
function add(o: mut Order, l: Line). An inner scope cannot declare a name again
(E_SHADOW). You cannot assign to a const. let x: T; declares a local with no value yet: an optional type starts
undefined, any other type its zero value (0, "", false, [], an empty Map, a record of zero fields), where
TypeScript refuses a read before the first assignment. A class instance has no zero value, so let c: C; is an error.
A const made from an object or array literal can change one step in: o.f = x, v[i] = x, v.push(x). This is
allowed only when nothing else holds the value: no other name, argument or lambda. A return or a spread may hold it.
Otherwise the const stays constant, because a copy would not see the change that TypeScript shows.
An object spread can call a function. The call runs once. It must be the first expression that runs in the statement and must run unconditionally. The compiler rejects a call spread when holding it before the statement would change this order.
Classes are objects, as in TypeScript: every name of an instance shares it, and === compares identity.
- The fields, and the
public x: Tparameters of a constructor, make the record.new Point(1, 2)runs the constructor. - A method gets
this, the instance, and changes it in place. - A getter
get area()reads asp.area, and a setterset area(v)runs forp.area = v. Static members are reached asPoint.origin(). - In the constructor,
thisexists from the statement where the last field with no initializer gets its value (this.x = x;). - A generic class
class Box<T>is generic in its record, constructor and methods:new Box(3)is aBox<i64>, and a method can have type parameters of its own. - There is no
extends(no inheritance).implementsnames interfaces that the methods implement.
class Counter {
constructor(private count: i64) {}
add(n: i64): void { this.count += n; }
get value(): i64 { return this.count; }
}
function main(): void {
const c = new Counter(1);
const d = c;
d.add(2);
print(`${c.value}`); // 3
}Enums are numbers or strings. enum Color { Red, Green = 5, Blue } makes Color.Blue 6, and the type Color is an
i64. enum Dir { Up = "UP" } makes a string. A literal union ("a" | "b", 1 | 2) is the type of its
literals.
An untagged union A | B (string | number, string[] | boolean) holds a value of one of its members. A value
of a member's type is one where the union is expected. typeof x === "string" (or !==) narrows x to that member,
as kind narrows a union: inside the if, and after an if that returns, once one member is left. typeof names
"number", "boolean", "string" or "object" (every other type), so two members of one name (number | i64, two
records) are an error: use a union with kind. typeof x is only compared with a name.
A tuple [A, B] (up to six items, labels allowed) is Tuple2<A, B> of gizlang:core. [a, b] makes one where it is
expected, and where nothing is expected its items have different types (new Map([["a", 1]])). t[0] reads an item.
const [a, , c] = t; binds items of a tuple or a Vec.
async and await: an async function runs to its end when it is called. So Promise<T> is T, and await e is
e: Promise.all(xs) and Promise.resolve(x) are their argument. Concurrent work uses Task (Task.zip, Task.thread).
Module-level let is state of the module, such as let count = 0; or let names: string[] = [];.
- Its functions read and assign it. Its initializer runs at its first use.
- It changes only by assignment (
names = [...names, n]). Only the functions of its own module reach it. - Under
giz liveit keeps its value across code changes.
A module const of one literal is that literal at each use. Any other value (const NAMES = ["a", "b"];, an
object, a call) is computed once, at its first use, and no assignment changes it; other modules read it too.
as, readonly: e as T is checked, as e satisfies T is (there is no unchecked cast). as const changes
nothing, and readonly on a field or a type changes nothing: a value is never changed through another name anyway.
match reads a union. Write one arm for each case and {} for no action. There is no
default arm. status.kind === "paid" (or !==) tests one case, and the compiler checks the name.
type Status = | { kind: "open"; } | { kind: "paid"; amount: i64; };
export function paid(status: Status): i64 {
match (status) { Status.open() => { return 0; } Status.paid(amount) => { return amount; } }
}status.kind === "paid" also narrows a const or parameter, so r.value reads the case:
- in
if (r.kind === "ok") { ... }; - after
if (r.kind === "err") { return ...; }, once one case is left (each suchifrules out its case).
Tests joined by &&, || and ! narrow too. In a.kind === "ok" && a.value > 0, the right side sees a ok. After
if (a.kind === "err" || a.value < 0) { return ...; }, a is ok. Inside one expression (?:, &&, ||, ?.) a
let narrows too, because no assignment can run there. An if block narrows only a const or parameter.
A while (true) (or for (;;)) with no break does not end, so a function needs no return after it.
TypeScript forms that lower onto the forms above:
`n=${n}`is"n=" + string(n). A hole takes whatstring(x)takes. The text may hold newlines;\`and\$are the characters.c ? a : bruns only the branch it picks.x ?? ytakes anOption<T>x (TypeScript'sT | undefined). It gives the value of x when there is one, else y. y runs only then.switch (x) { case a: case b: ...; break; default: ... }is anif (x === a || x === b)chain. A case ends withbreak,returnorcontinue, or is the last. There is no fall-through into a case with statements, andbreakleaves the switch only at the end of a case.- A
switch (u.kind)whose cases name every case of the union, each ending inreturn, needs nodefault. It also needs noreturnafter it, as in TypeScript. A missed case is E_RETURN. x => eand(a, b) => etake their parameter types from the expected function type.if (c) f();,else g();and loops take one statement without braces.a ||= b,a &&= b.- An integer literal beside an
f64is anf64:x / 2,n === 0.
Errors work as in TypeScript, with the type in the signature.
function f(): T throws Ereturns aResult<T, E>.throw e;fails it.- Without
throws, a function whose ownthrownotryblock in it catches throws what it throws: X forthrow new X(...),stringfor a string, elseError. A call of it propagates the same way only where the caller throws too. - A call gives the
T. A failure returns from the caller. try { ... } catch (e) { ... }handles a failure.finallyruns when the statement ends.- In a function that does not throw, a call that fails outside a
tryblock faults (R004). - The standard
Error(name,message,cause) needs no import:throw new Error("bad input")in athrows Errorfunction, ande.messagein its catch. - A thrown Error or string that nothing catches is printed after the fault trace.
See LANGUAGE: Errors.
function half(n: i64): i64 throws Error { if (n % 2 !== 0) { throw new Error(`odd: ${n}`); } return n / 2; }
function quarter(n: i64): i64 throws Error { return half(half(n)); }Errors that are values are Result<T, E> and Option<T> from gizlang:core (Result.ok(v),
Result.err(e), Option.some(v), Option.none()). try e unwraps ok or some. It
returns any other case from the function. The result type of the function must have that case.
import {Result} from "gizlang:core";
type Line = { sku: string; qty: i64; };
function find(id: string): Result<Line, string> {
if (id === "") { return Result.err("no id"); }
return Result.ok({ sku: id, qty: 1 });
}
export function total(id: string): Result<i64, string> { const o = try find(id); return Result.ok(o.qty); }Use opt ?? fallback, res.valueOr(fallback) and x!. x! faults when
the value is absent.
Loops: for (const x of v), for (const i of range(0, n)), for (let i = 0; i < n; i++) (a continue runs the
update first), while, break, continue. i++, i--, ++i and --i are statements, not values.
for (const [k, v] of m) and for (const { a } of xs) destructure each element. A Map gives its [key, value]
entries and a Set its values, in insertion order. A label (outer: for (...)) names the loop that
break outer; and continue outer; leave. a ??= b, a ||= b and a &&= b assign as in TypeScript. A parameter
with a literal default and no type (greeting = "hi") has the literal's type. ${x} of an optional prints its value
or undefined.
Optional values are Option<T> of gizlang:core, with no import. x?: T (a field or parameter), T | undefined
and T | null are Option<T>.
- Where an Option is expected,
undefined(ornull) is none, and aTis some of it. So these work:return undefined;,{ name: "a" }without an optional field,Options("h", 5)without the trailing optional fields, and a call without an optional parameter. a?.b,a?.[i]anda?.m()give the Option of the rest of the chain.aruns once, in its order in the expression.x!isx.unwrap(): a fault when there is none.x === undefinedandx !== undefinedtest the case and narrowx, asx.kindtests do. Wherexis some,xreads its value (x.value).x.kind,x.valueand the Option methods still read the Option.- A parameter default is a literal,
[]orundefined:function f(n: i64 = 1, tag?: string).
Destructuring: const { a, b: c } = e; and const [a, , c] = e; (also with let) and a parameter { x, y }: Point
bind fields or items; e runs once. A last parameter ...xs: T[] takes the rest of the arguments as one array, and
f(...v) passes an array whole. let a = 0, b = 1; declares both. Numbers take _ between digits: 1_000_000.
Interfaces of fields (interface User { id: i64; email?: string; }) are record types, as type User = { ... }.
Interfaces of methods are the interfaces of TypeScript, with self: Self written first. As a type,
an interface holds any value that implements it. Arrow functions (x: T): R => { ... }
capture const values by value. A let that an arrow function names is shared, as in TypeScript: a change on
either side is seen by both, and each pass of a loop has its own. A block body with no return of a value is void. Function types (x: T) => R and => void are value types:
fields, Vec elements, parameters and results.
A module constant that is an arrow function (const inc = (x: i64) => x + 1;) is a function declaration.
Console: console.log(a, b) (also info, debug, trace) prints its arguments joined by spaces, and
console.error and console.warn print them on standard error. An async function may return Promise<void>.
Built-in methods need no import, as in TypeScript. A lambda takes its parameter types from the method, with no annotation, and compiles to a direct call.
- Arrays:
v.filter(f),v.map(f),v.find(f)(anOption),v.findIndex(f)(-1 when none),v.some(f),v.every(f),v.count(f),v.reduce((acc, x) => ..., init),v.includes(x),v.indexOf(x),v.at(i)(anOption; -1 is the last),v.concat(w),v.slice(a, b),v.push(x),v.pop()andv.shift()(anOption, in place),v.join(sep)(numbers and bools too),v.sumBy(f),v.findLast(f),v.findLastIndex(f),v.lastIndexOf(x),v.flat(),v.flatMap(f),v.toReversed(),v.with(i, x),v.toSpliced(i, n, ...items),v.keys(),v.entries(); in place on alet:v.reverse(),v.splice(i, n, ...items)(gives the removed items),v.unshift(...items),v.fill(x). v.toSorted(compare)andv.sort(compare)(in place, on alet) take a TypeScript comparator, negative whenagoes first ((a, b) => a - b,a.name.localeCompare(b.name)), or a boolbefore(a, b), true whenagoes first.giz fmtwrites the former namev.sorted(f)asv.toSorted(f).new Map<K, V>()withset,get(anOption),has,delete,size,keys,valuesandmerge;new Set<K>()withadd,has,delete,sizeandvalues.new Map([[k, v], ...])andnew Set([a, b])start with those items, and[...s]spreads a Set (or a Map's values) as it spreads a Vec.new Map()takes its type arguments from the type expected.m[k]ism.get(k)andm[k] = vism.set(k, v).Array.from(x)makes a Vec of a Vec, a Set or a Map's values, andArray.from({ length: n }, (_, i) => f(i))makesnitems.- A regex literal
/a+b/uis a gizlang:regexRegex(test,exec,matchAll,replaceAll,split), with no import. A pattern that does not compile faults when it runs. JSON.stringify(x)writes any value whose type is known: a record as an object of its fields (anundefinedfield is left out), a union's case with itskind, an untagged union's case as its value, a Vec as an array, aMap<string, V>as an object andundefinedasnull.JSON.stringify(x, null, n)indents.JSON.parse(s)reads the type expected (const u: User = JSON.parse(s);): a missing field is an error unless its type takesundefined, an unknown field is skipped, and a wrong value throws a gizlang:jsonJsonError. Where no type is expected it reads aJsonvalue. A class instance is not written or read yet.- An object type
{ a: T; b?: U }is a record of those fields; two of the same fields are one type. Record<K, V>isMap<K, V>, and{}where one is expected is an empty one. Over a record type,Partial<T>makes each field anOption,Readonly<T>isT, andPick<T, "a" | "b">andOmit<T, "a">keep those fields or the others.new X(...)calls the functionnewX(...).new ns.X(...)calls the exportednewXof the module thatnsnames. Aconstmade bynewchanges in place as one made by a literal does.- A method with no other parameter also reads as a property:
m.size. - Strings:
indexOf,includes,startsWith,endsWith,split,replaceAll,trim,padStart,toUpperCase,charAt,substring,localeCompare, ... Numbers:x.toFixed(digits). - TypeScript's ambient functions:
Math.max(a, b, ...),Math.min,Math.PI,Math.Eand the otherMathfunctions (an integer argument is anf64;Math.maxandMath.minof integers are integers),Number.isInteger,Number.isNaN,Number.isFinite,String(x),Object.keys(record)(its field names), andparseInt(s, radix)andparseFloat(s), which give anOption(parseInt(s) ?? 0) where TypeScript givesNaN.console.logand${x}print anOptionas its value orundefined. - Import the rest by name:
gizlang:text(parseI64, a unionIntegerParse.ok(n)/invalid/overflow,parseF64, anOption, andfixed(x, digits)) andgizlang:math(floor,round,min,max,pow, ...).
Markup is TSX that compiles to Html (gizlang:html). It is escaped by default.
import {Html, each, when} from "gizlang:html";
type Todo = { id: string; title: string; done: bool; };
function Row(t: Todo): Html { return <li class="todo" data-k={t.id}>{t.title}{when(t.done, () => <b>done</b>)}</li>; }
type ListProps = { title: string; };
function List(p: ListProps, children: Html): Html { return <section><h2>{p.title}</h2>{children}</section>; }
export function page(todos: Vec<Todo>): Html {
return <List title="Todos"><ul>{each(todos, (t: Todo) => Row(t))}</ul></List>;
}Holes take text, numbers and Html. A bool attribute is present or absent. Markup has no
spread, &&, ? : or .map. Use when, each and raw(trusted). h.text() is the
response body.
Functions: function f(x: T): R { ... }, and : void for no result. Declare a method as
function Order.total(self: Order): i64 and call it as o.total(). The compiler infers effects.
Tests are export function testName(): void in a *.test.giz file. They assert with
import * as Test from "gizlang:test" (Test.equal(a, b), Test.check(c)). giz test FILE runs them.
Optional results. find gives an Option rather than a -1 index, and ?? takes a missing
value's fallback lazily. Task.zip(first, second) starts two independent tasks and joins both,
returning a typed pair or Fault.
Map optional and successful results. option.map((value: T) => f(value))
returns Option<U> without unwrapping. option.andThen(f) chains a function
that returns Option<U>; result.map(f) changes only the ok value and
preserves err. The compiler dispatches Vec.map to its vector builtin and other map calls to their typed receiver
methods.
Inferred callback results. For Option<T> or Result<T, E>, .map(x => f(x))
infers both the x: T input and the result type from f(x). Generic callback
parameters can use the same syntax after earlier call arguments establish their
types. The compiler keeps effect checking and preserves absent/error branches.
Imported functions are receiver-callable. An imported free function whose first argument
accepts the receiver is also a method: x.f(a) calls f(x, a), with its signature, effects
and mut rules. A generic callback parameter bounded by a function type
(F extends (value: T) => bool) gives a lambda its parameter types, and F stays the lambda's
own closure type.
Composing fallible results. result.mapErr(e => message(e)) converts only
an error. result.andThen(x => next(x)) chains another Result with the same
error type. try parser(input).mapErr(e => explanation(e)) propagates a mapped
error without a temporary default and a two-arm match.