Skip to main content
Version: 0.3

null and absent values

xisto has one empty value: null. T | null is the nullable type. There is no separate undefined value: where code writes undefined, the compiler reads null and warns.

x.ts:1:29: warning[XS2101]: 'undefined' is deprecated in xisto: it is read as null
1 | let s: string | undefined = undefined;
help: write null: xisto has one empty value

T | undefined and x?: T are accepted and mean T | null. null alone is not a type (XS2008).

Why​

Two empty values cost a check per read and a representation per type. Real code rarely means two different things by them. A nullable int, number or boolean uses a sentinel value inside the same 8 bytes, so int | null costs nothing extra.

Optional fields and "absent"​

An optional field that was never given is absent. It is left out of console.log and JSON, and in is false, as on Node:

interface User { name: string; nick?: string; age?: int; admin?: boolean }
const a: User = { name: "ana" };
console.log(a, JSON.stringify(a)); // { name: 'ana' } {"name":"ana"}
console.log("nick" in a); // false
console.log(a.nick ?? "(none)", a.nick?.length); // (none) undefined

An absent field reads as null. So a.admin === null is true in xisto and false on Node. Test emptiness with x == null, which is the same on both.

?., ??, ??=​

All three work on T | null, including nullable primitives:

let c: string | null = null;
c ??= "set";
const n = a.age ?? 0; // int
const f = a.admin ?? false; // boolean

Both sides of ?? must have the same type: a.admin ?? "none" is an error.

Narrowing​

A nullable value is narrowed by !==/=== against null, ==/!=, &&, || and !, as in TypeScript:

function len(s: string | null): int { if (s === null) return 0; return s.length; }
let cur = head;
while (cur !== null) { total += cur.value; cur = cur.next; }
if (box.inner && box.inner.label !== null) console.log(box.inner.label.length);

Field paths are narrowed inside the block. An assignment undoes the narrowing. A variable that a closure captures and reassigns lives in a heap cell; inside a function it is narrowed too, and each read after the test is checked at run time:

runtime error: TypeError: 'last' is null here: a call made since it was checked set it back to null

x! and (x as T) are checked: null there is a TypeError, where TypeScript would go on with null.

What prints undefined​

console.log prints undefined where Node would: an absent optional field read through ?., an unmatched regex group. A null you stored prints null. Map.get of a missing key returns null for reference values (Node: undefined); for int values it is a run-time error ("Map.get: key not found (check with has() first)").

Comparing with null​

Comparing a value that is never null with null compiles with warning XS2102 ("int is never null: this comparison is always false"). Ordering comparisons (<, >) on int | null are refused: narrow first.

See also​