Coming from JavaScript
Every JavaScript habit beside its Essence spelling and the error you get if you forget, in one lookup table per topic.
Each row of a topic table pairs a JavaScript or TypeScript habit with its Essence spelling and the error you get if you write the habit instead. A dash in the error column means the habit carries over or has no error. Nothing here is explained: each Essence cell links to the page that explains it, and each error code links to its entry in the diagnostics reference. To look up a message the compiler printed, see What the compiler says when you type JavaScript.
Here is a five-line TypeScript function, then the same function in Essence. Both drop the empty names and greet the rest.
function greeting(names: string[]): string {
const known = names.filter((name) => name.length > 0)
if (known.length === 0) return "Hello, nobody"
return `Hello, ${known.join(" and ")}`
}
console.log(greeting(["Ada", "", "Grace"])) // Hello, Ada and Grace
console.log(greeting([])) // Hello, nobody
implementation {
function greeting(to names: List<String>) -> String {
constant known = names::everyItem(where (name) {
<- name::hasCharacters()
})
if known::isEmpty() {
<- "Hello, nobody"
}
<- "Hello, {known::join(with " and ")}"
}
Terminal.print(greeting(to ["Ada", "", "Grace"])) § Hello, Ada and Grace
Terminal.print(greeting(to [])) § Hello, nobody
}
This program uses parts the language pages teach later. Programs and values teaches implementation, the § comments, constant, string and list literals, List<String> and the {…} interpolation. Method calls teaches :: calls and their labels, and Conditions teaches if. Functions teaches function, the to label, -> String and <-, and Functions as values teaches the (name) { … } literal. The library calls are everyItem(where:), isEmpty(), join(with:), hasCharacters() and Terminal.print.
Five differences show at once. A method is called with ::, and . calls a static such as Terminal.print. A function’s return type is required, where TypeScript would infer it. <- returns, inside the callback too. Each call writes its labels: to, where, with. And the comparisons > 0 and === 0 are the methods hasCharacters() and isEmpty().
Bindings
constant and variable replace const and let. A variable can be reassigned, and a record, a list or a dictionary is never changed in place.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
const total = 1 |
constant total = 1 |
foreign-syntax |
let count = 0, var count = 0 |
variable count = 0 |
foreign-syntax |
a ; at the end of a statement |
constant total = 1, with no ; |
foreign-syntax |
count += 1 |
count = count::add(1) |
operator-not-supported |
assigning to a const |
declare it with variable |
constant-reassignment |
let count = 0, then count = "none" |
a variable keeps the type it was declared with |
assignment-type-mismatch |
let count: number, assigned later |
the value is part of the declaration: variable count: Integer = 0 |
foreign-syntax, then syntax-error for the missing value |
user.name = "Ada" |
a new record, Essence’s object: { user with name = "Ada" } |
foreign-syntax |
const { x, y: row } = point |
constant { x, y as row } = point |
foreign-syntax for the const; unknown-type for constant { x, y: row } = point, since : annotates a type |
reading a const above its line |
refused, as in the temporal dead zone | unknown-name |
snake_case names |
camelCase; _ cannot appear in a name |
syntax-error |
// note, /* note */ |
§ note |
foreign-syntax |
/** Documentation. */ |
§§ Documentation. |
foreign-syntax |
Operators
There are none. Arithmetic, comparison and logic are method calls, read left to right.
Every row of this table reports operator-not-supported, whose helps name the method for the operator you wrote. Only the last row is something else.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
a + b |
a::add(b) |
Essence has no '+' operator |
a - b |
a::subtract(b) |
Essence has no '-' operator |
a * b |
a::multiply(with b) |
Essence has no '*' operator |
a / b |
a::divide(by b), an Optional (a value that may be missing) unless b is known not to be zero |
Essence has no '/' operator |
a % b |
a::remainder(dividingBy b) |
Essence has no '%' operator |
a === b, a == b |
a::is(b), which compares records and lists by content |
Essence has no '===' operator |
a !== b |
a::isNot(b) |
Essence has no '!==' operator |
a < b, a <= b |
a::isLessThan(b), a::isLessThanOrEqualTo(b) |
Essence has no '<' operator |
a > b, a >= b |
a::isGreaterThan(b), a::isGreaterThanOrEqualTo(b) |
Essence has no '>' operator |
a && b |
a::and(b); both sides are evaluated, so nest an if when the right side must not run |
Essence has no '&&' operator |
a || b |
a::or(b); both sides are evaluated |
Essence has no '||' operator |
!a |
a::negate() |
Essence has no '!' operator, and the help names a |
-x |
x::negate(); - only signs a number literal |
Essence has no '-' operator |
(a + b) * c |
a::add(b)::multiply(with c); there are no grouping parentheses |
Essence has no '+' operator, at the + inside the parentheses |
c ? a : b |
define { as a if c as b otherwise } |
foreign-syntax |
Numbers
There is no number and no floating point. An Integer is a whole number with no largest value; a Rational is an exact fraction.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
number |
Integer or Rational |
unknown-type |
1n |
1: every Integer is arbitrary precision |
invalid-number |
0.75 |
0.75, an exact Rational that prints as 3/4 |
— |
0.1 + 0.2 === 0.3 is false |
0.1::add(0.2)::is(0.3) is true |
— |
10 / 4 is 2.5 |
10::divide(by 4) is 5/2 |
operator-not-supported |
-7 % 3 is -1 |
-7::remainder(dividingBy 3) is 2; -7::remainder(dividingBy 3, as #Truncating) is -1 |
operator-not-supported |
Math.floor(7 / 2) |
7::quotient(dividingBy 2) is 3 |
operator-not-supported at the /; foreign-syntax for Math |
Math.max(a, b), Math.min(a, b) |
Number.highest(a, b), Number.lowest(a, b) |
foreign-syntax |
Math.abs(n), Math.round(x) |
n::absolute(), x::round(), which rounds halves away from zero: -7/2::round() is -4, where Math.round(-3.5) is -3 |
foreign-syntax |
NaN, Infinity |
none; a division that can fail returns an Optional |
foreign-syntax |
parseInt("42") |
Integer.parse("42") is #Value(42); Integer.parse("4x") is #Empty |
foreign-syntax |
n.toFixed(2) |
n::toString(as #Decimal, toPlaces 2) |
type-without-members |
0xff, 1e3 |
decimal digits only: 255, 1_000 |
invalid-number |
Strings
Double quotes only, and every string literal interpolates. length() counts characters as a reader sees them.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
'hi', `hi` |
"hi" |
foreign-syntax |
`Hello, ${name}` |
"Hello, {name}" |
none: "Hello, ${name}" compiles and prints a $ before the value |
'{ "a": 1 }' |
"\{ \"a\": 1 }": a literal { is written \{ |
unclosed-string for "{ \"a\": 1 }" |
a + b |
a::append(b), or "{a}{b}" |
operator-not-supported |
s.length counts UTF-16 units: "🇩🇪".length is 4 |
s::length() counts characters: "🇩🇪"::length() is 1 |
method-called-with-dot |
s[0], s.charAt(0) |
s::character(at 0), an Optional<Character> |
foreign-syntax for s[0], at the brackets; type-without-members for s.charAt(0), since a String has no charAt under any spelling |
s.toUpperCase() |
s::uppercase() |
type-without-members; unknown-method for s::toUpperCase() |
s.trim(), s.includes(t) |
s::trim(), s::contains(t) |
method-called-with-dot for trim; type-without-members for includes |
s.split("\n") |
s::split(on "\n") |
method-called-with-dot |
s.indexOf(t) is -1 when absent |
s::firstIndex(of t) is #Empty when absent |
type-without-members |
"café" === "cafe\u0301" is false |
::is returns true for the same two spellings of é |
— |
"\u00e9" |
"\u{e9}" — the braces are not optional, and they take one to six digits |
unbraced-unicode-escape, whose help rebrackets the digits you wrote |
"\uD83D\uDE00", a surrogate pair |
"\u{1F600}", one escape for one character |
unbraced-unicode-escape; "\u{D83D}" alone is surrogate-unicode-escape |
Collections
Records, lists and dictionaries are all immutable. A method that would change one returns a new one.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
{ x: 1, y: 2 } |
{ x = 1, y = 2 } |
foreign-syntax |
{ ...point, x: 9 } |
{ point with x = 9 } |
syntax-error |
{ x, y }, point.x |
the same | — |
[1, 2, 3] |
the same | — |
items[0], items.at(-1) |
items::item(at 0), items::item(at -1), an Optional |
foreign-syntax for items[0], at the brackets; type-without-members for items.at(-1) |
items.length |
items::length() |
method-called-with-dot |
items.push(4), [...items, 4], items.concat(more) |
items::append(4), items::append(contentsOf more), a new list |
type-without-members, since a list has no push under any spelling; syntax-error for [...items, 4] |
items.pop(), items.shift() |
items::lastItem() reads the item and items::removeLast() returns the rest; firstItem() and removeFirst() at the front |
type-without-members; unknown-method for items::pop() |
items.map(f) |
items::map(f) |
method-called-with-dot |
items.map((x, i) => …) |
items::enumerate(), then map reading entry.index and entry.item |
foreign-syntax as written; uninferable-parameter-type, then no-matching-overload, for items::map((x, i) { … }) |
items.reduce(f, 0) |
items::reduce(startingWith 0, f) |
method-called-with-dot |
items.filter(p) |
items::everyItem(where p) |
unknown-method for items::filter(p) |
items.find(p), items.findIndex(p) |
items::firstItem(where p), items::firstIndex(where p), an Optional |
unknown-method for items::find(p) |
items.indexOf(x) is -1 when absent |
items::firstIndex(of x), an Optional |
unknown-method for items::indexOf(x) |
items.some(p), items.every(p) |
items::hasItems(where p), items::hasOnlyItems(where p) |
type-without-members; unknown-method for items::some(p) |
items.includes(x) |
items::contains(x) |
unknown-method for items::includes(x) |
items.slice(1, 3) |
items::slice(from 1, to 3) |
no-matching-overload for items::slice(1, 3) |
items.join(", ") |
items::join(with ", ") |
method-called-with-dot |
items.flat() |
items::flatten() |
unknown-method for items::flat() |
items.sort(), items.toSorted() |
items::sort(), a new list in the items’ own order |
unknown-method for items::toSorted() |
items.sort((a, b) => a - b) |
items::sort(by (a, b) { <- a::compare(to b) }), where the function returns an Ordering |
foreign-syntax as written; no-matching-overload without by |
items.reverse() |
items::reverse(), a new list |
method-called-with-dot |
names.map((name, i) => [name, prices[i]]) |
names::pair(with prices), a list of { first, second } records |
foreign-syntax as written; unknown-method for names::zip(prices) |
const [first, second] = items |
none; read items with item(at:), since there is no list pattern |
syntax-error for constant [first, second] = items |
new Map(), Record<string, number> |
a Dictionary: ["ann" = 31], or [=] when empty |
foreign-syntax for new and for Map; type-not-generic for Record<String, Integer>: Essence’s Record takes no type arguments |
ages.get("ann") |
ages::value(at "ann"), an Optional |
type-without-members |
ages.set("bob", 40) |
ages::set("bob", to 40), a new dictionary |
method-called-with-dot |
new Set(items) |
a list, with removeDuplicates() and contains |
foreign-syntax |
Null, undefined and exceptions
There is no null and no undefined. A value that may be missing is an Optional; an operation that can fail with a reason returns a Result.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
null, undefined |
Optional<T>: #Value(x) or #Empty |
foreign-syntax |
string | null |
Optional<String> |
unknown-type |
x ?? d |
x::value(defaultingTo d) |
operator-not-supported |
user?.name |
user::map(.name), still an Optional; andThen when the step returns an Optional |
foreign-syntax |
user! |
none; take the value out with match or value(defaultingTo:) |
foreign-syntax |
if (user) |
if user::hasValue(), a Boolean; it does not narrow user, so read the value with a match |
syntax-error as written; condition-not-boolean for if user |
user.name on a value that may be missing |
refused: an Optional is taken apart, never reached through |
type-without-members; unknown-method for a :: call |
throw new Error("…") |
return #Failure(reason) in a Result<T, F> |
foreign-syntax |
try { … } catch (error) { … } |
recover(with:), reason(), or a match |
foreign-syntax |
Functions
A function declaration writes the type of every parameter and its return type. A call writes each label, in order.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
function add(a, b) { … } |
function add(_ a: Integer, _ b: Integer) -> Integer { … }; _ means the call passes the argument with no label |
syntax-error |
| a function with no return type | -> Type is always written; -> {} returns nothing, like : void |
syntax-error |
return x |
<- x |
foreign-syntax, and the function still returns, so nothing cascades off it |
| a path that falls off the end | every path returns | missing-return |
(n) => n + 1 as an argument, with its implicit return |
(n) { <- n::add(1) }; a value leaves only through <- |
foreign-syntax; uninferable-return-type without the <- |
const double = (n) => n * 2 |
constant double = (_ n: Integer) -> Integer { <- n::multiply(with 2) }; only an argument infers its types |
foreign-syntax twice, for the const and the =>; uninferable-parameter-type and missing-return-type once it is written with braces |
greet({ name: "Ada", loud: true }) |
labels, in declaration order: greet(name "Ada", loud true) |
foreign-syntax as written; argument-label-mismatch for greet("Ada", true) or labels out of order |
function f(a, b = 1) |
function f(_ a: Integer, b: Integer = 1), called as f(1, b 5) |
argument-label-mismatch for f(1, 5) |
(product) => product.price |
.price |
— |
| overload signatures | overload entries inside a namespace, each with its own body |
— |
function first<T>(items: T[]): T |
function first<infer T>(…); TypeScript’s infer keyword is unrelated |
uninferred-type-parameter at the declaration |
identity<string>(x) |
none; every type argument is inferred | foreign-syntax |
Control flow
if is a statement and produces no value. define is the conditional expression, and match branches on a value’s case; both produce a value. There is no loop statement.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
if (c) { … } else if (d) { … } else { … } |
if c { … } else if d { … } else { … }, with no parentheses |
syntax-error |
if (count) |
if count::isNot(0); only a Boolean is a condition |
syntax-error as written; condition-not-boolean for if count |
an if used as a value |
define { as a if c as b otherwise } |
syntax-error |
switch (x) { case …: } |
match x -> T { case … }, exhaustive, with its result type written, -> T |
foreign-syntax for switch; missing-case when a case is left out |
default: |
case _ |
— |
for (const item of items) |
items::map(f), reduce, or loop(from 1, through 10, startingWith 0, f) |
syntax-error; uninferable-return-type for a function passed to map with no <- |
items.forEach(f) |
none; to print each item, print items::join(with "\n") once, or map with a function that returns: items::map((item) { <- Terminal.print(item) }) |
type-without-members for items.forEach(f); unknown-method for items::forEach(f); uninferable-return-type for a function with no <- |
while (c) { … } |
loop(startingWith s, while p, f), where p is a function from the state to a Boolean |
foreign-syntax, whatever the body holds |
break, continue |
loop(startingWith s, step f), where f returns #Done(value) to stop and #Continue(next) to go round again |
foreign-syntax |
Types
A type names a shape and is structural. A choice is a closed set of cases and is nominal. There are no classes.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
type Point = { x: number } |
type Point = { x: Integer }; a record with more members still fits |
unknown-type |
interface Point { x: number } |
type Point = { x: Integer } |
foreign-syntax |
{ kind: "circle", radius } | { kind: "square", side } |
choice Shape { Circle { radius: Rational }, Square { side: Rational } }, built as #Circle({ radius = 1/2 }) |
missing-case when a match leaves a case out |
"fast" | "slow" |
choice Speed { Fast, Slow }; there are no literal types |
syntax-error |
enum Color { Red, Green } |
choice Color { Red, Green }, used as #Red; to print one, declare namespace Colors for Color is Printable {} |
foreign-syntax for enum; unsatisfied-bound when printed without that namespace |
Red for a case of choice Color |
#Red; the # is what tells a case from a name |
unknown-name, naming every choice that declares the case |
A | B |
A | B, the same; narrowed by match, never by typeof |
foreign-syntax for typeof |
[string, number] |
a record with two named members; there are no tuples | syntax-error |
class |
a type for the shape and a namespace for its methods |
foreign-syntax |
value as Point |
constant point: Point = value; there is no cast |
unknown-name |
readonly, Readonly<T> |
nothing to write; records, lists and dictionaries are immutable | — |
type Tree = { children: Tree[] } |
none; a type or choice cannot name itself |
recursive-type-declaration |
T extends Comparable |
<infer T is Comparable> |
syntax-error |
| a branded type checked by a type guard | a checked refinement: type Positive = Integer where @::isPositive(), proven by an if |
— |
Namespaces and protocols
Methods live in namespaces, outside the type. A protocol is a contract: a namespace declares it meets one with is Named (a conformance), and a type parameter requires one with <infer T is Named> (a bound).
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
value.method() |
value::method() |
method-called-with-dot where the method exists; type-without-members where it does not |
Money.of(12) |
Money.of(12): . calls a static, as in JavaScript |
static-method-on-value when called with :: on a value |
this |
@ |
foreign-syntax |
class Money { euros() { … } }, Number.prototype.euros |
namespace Money for Integer { euros() -> Integer { … } } |
foreign-syntax, once; the class body is read past rather than into |
| using a class above its declaration | a namespace cannot be used by a statement above it | use-before-declaration |
static rate = 100 |
static rate = 100, read as Money.rate |
— |
interface Named { name(): string } |
protocol Named { name() -> String } |
foreign-syntax |
class Person implements Named |
namespace People for Person is Named; having the method is not conforming |
syntax-error for implements; unsatisfied-bound without is Named |
| a concrete method on an abstract class | a method with a body in the protocol | — |
interface Loud extends Named { shout(): string } |
protocol Loud is Named |
foreign-syntax, naming the extends |
Named as a type, Named[] |
none; a protocol is a bound or a conformance, and a union such as Book | Movie stands in, reaching name() when both types conform |
protocol-as-type |
T extends A & B |
one bound per parameter; protocol Both is A, is B combines them |
syntax-error |
Modules
A file is a module with named exports, the way ES modules work.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
import { a, b as c } from "./x" |
import { from "./x.es" { a b as c } }, with no commas |
syntax-error |
"./x" with no extension |
"./x.es" |
invalid-module-specifier |
export function f … |
export { f }, in a block below implementation |
unknown-name inside implementation; unexpected-token below it |
export default f |
none; every export is named | unexpected-token |
export { a } from "./x" |
from "./x.es" { a } inside the export block |
— |
import * as money from "./x" |
none; each name is imported | syntax-error |
import { chunk } from "lodash" |
none; a specifier is a relative path to an .es file |
invalid-module-specifier |
require("./x") |
none; import only |
foreign-syntax |
| two modules importing each other | allowed; a constant of the other module is read only once the modules have loaded | cyclic-load-time-use |
Tooling
One essence command does the work of several tools, and tests live in the source file.
| JavaScript or TypeScript | Essence | If you forget |
|---|---|---|
tsc --noEmit |
essence check |
— |
esbuild --bundle |
essence build, one self-contained JavaScript file |
— |
node file.js, tsx file.ts |
essence run file.es |
— |
vitest, jest |
essence test; the tests are a tests { … } section at the end of the file |
test-outside-tests |
describe, it, expect(x).toBe(y) |
suite "…", test "…", expect x::is(y) |
foreign-syntax at the => of describe("…", () => …); unknown-name for expect(x).toBe(y) |
tsconfig.json |
essence.json, optional, written by essence init |
— |
prettier |
essence format, with nothing to configure |
— |
console.log(x) |
Terminal.print(x) |
foreign-syntax |
console.dir(x) |
Terminal.inspect(x) |
foreign-syntax |
What the compiler says when you type JavaScript
Most of these errors name the JavaScript habit they refuse, and the table below pairs each with its fix. Each error starts with its code in brackets and a one-line message, then points at the code, like this one for constant total = 1 + 2:
[operator-not-supported]
Error: Essence has no '+' operator
╭─┤ Total.es:2:21 │
│
2 │ constant total = 1 + 2
│ ▲
│ ╰── this is not a Method call
│
│ Help 1: Write 'a::add(b)' for Numbers.
│ Help 2: Write 'a::append(b)' to join two Strings.
│
│ Note: Arithmetic, comparison and logic are Method calls in Essence, read left to right — there are no operators, and so no precedence to remember.
───╯The table lists the errors you are likely to hit first: what you typed, the error’s code and message, and the fix. Where your editor offers a Quick Fix, it writes the fix for you.
| You typed | The compiler says | The fix |
|---|---|---|
1 + 2 |
operator-not-supported: Essence has no '+' operator |
1::add(2) |
"a" + "b" |
operator-not-supported: Essence has no '+' operator |
"a"::append("b") |
count / 3 |
operator-not-supported: Essence has no '/' operator |
count::divide(by 3) |
1 == 1, 1 === 1 |
operator-not-supported: Essence has no '==' operator |
1::is(1) |
true && false |
operator-not-supported: Essence has no '&&' operator |
true::and(false) |
!true |
operator-not-supported: Essence has no '!' operator |
true::negate() |
-price |
operator-not-supported: Essence has no '-' operator |
price::negate() |
Terminal.print(items[0]) |
foreign-syntax: A value is not indexed with brackets |
Terminal.print(items::item(at 0)) |
text.length |
method-called-with-dot: 'length' is a Method, not a member |
text::length() |
text.charAt(0) |
type-without-members: This value has no members to look up |
text::character(at 0) |
const total = 1 |
foreign-syntax: 'const' is not how Essence declares a binding |
constant total = 1 |
let count = 0 |
foreign-syntax: 'let' is not how Essence declares a binding |
variable count = 0 |
constant total = 1; |
foreign-syntax: A Statement does not end with ';' |
constant total = 1 |
{ x: 1, y: 2 } |
foreign-syntax: A Record member is written with '=' |
{ x = 1, y = 2 } |
point.x = 2 |
foreign-syntax: A Record's member can not be assigned |
{ point with x = 2 } |
return n |
foreign-syntax: 'return' is not how Essence answers with a value |
<- n |
(n: Integer) => n |
foreign-syntax: A Function literal has no '=>' |
(_ n: Integer) -> Integer { <- n } |
[1, 2]::map(n => n) |
foreign-syntax: A Function literal has no '=>' |
[1, 2]::map((n) { <- n }) |
(n) { n::add(1) } |
uninferable-return-type: The return Type could not be inferred from the body |
(n) { <- n::add(1) } |
true ? 1 : 2 |
foreign-syntax: Essence has no conditional operator |
define { as 1 if true as 2 otherwise } |
identity<Integer>(1) |
foreign-syntax: A call writes no Type Arguments |
identity(1) |
if (count::isEven()) { |
syntax-error: Expected a Type but found ':'. |
if count::isEven() { |
if (ready) { |
syntax-error: Expected '{' but found '}'. |
if ready { |
`hi` |
foreign-syntax: A String is written in double quotes |
"hi" |
'hi' |
foreign-syntax: A String is written in double quotes |
"hi" |
// a comment |
foreign-syntax: A Comment is written with '§' |
§ a comment |
console.log("hi") |
foreign-syntax: 'console' names nothing in Essence |
Terminal.print("hi") |
null |
foreign-syntax: 'null' names nothing in Essence |
#Empty, in an Optional |
Red, for a case of choice Light |
unknown-name: 'Red' is not declared, with a note naming Light |
#Red |
number as a type |
unknown-type: Type 'number' is not declared |
Integer or Rational |
interface Point { x: Integer } |
foreign-syntax: 'interface' declares nothing in Essence |
type Point = { x: Integer } |
constant snake_case = 1 |
syntax-error: Expected '=' but found '_'. |
constant snakeCase = 1 |
Same as JavaScript
These need no relearning. Each carries its one caveat.
- Function hoisting. A function can be called above its declaration. Caveat: a namespace cannot be used by a statement above it.
- The constant’s dead zone. A constant cannot be read above its line, the way
consthas a temporal dead zone. Caveat: a function body written above the constant cannot read it either. { x, y }shorthand. It builds a record from the names in scope. Caveat:{ point with x }merges a record namedx; it is not shorthand forx = x.- Structural object types. A record with more members passes where a narrower
typeis asked for, and keeps them. Caveat: a literal written where a type is declared is checked for extra members, as TypeScript’s excess-property check does; a value passed along keeps them. - Renames with
as.constant { y as row } = pointandfrom "./Money.es" { twice as double }. Caveat: in a pattern,:annotates a type instead of renaming. - ES-module shape. A file imports by relative path and exports by name. Caveat: the
.esextension is written, and there is no default export. - Default parameters are evaluated per call, in the callee. A default runs on every call that leaves its argument out, and may read the parameters to its left. Caveat: a call that passes the argument still writes its label.
reduceis accumulator-first.[1, 2, 3]::reduce(startingWith 0, (total, item) { <- total::add(item) })is6. Caveat: the start value comes first, labelledstartingWith.1_000. Digit groups are separated with_, after a decimal point too:1_000.000_5. Caveat:_belongs to numbers only, sosnake_caseis not a name."".split("\n")is[""].""::split(on "\n")prints[""]. Caveat: the separator is labelledon.
What is not in the language
Each entry names what stands in its place, where anything does.
- Operators. Arithmetic, comparison and logic are method calls.
nullandundefined. A value that may be missing is anOptional.- Truthiness. A condition is a
Boolean;if countis refused ascondition-not-boolean. - Exceptions. There is no
throw,tryorcatch; a failure with a reason is aResult. - Mutation of data. Records, lists and dictionaries are immutable;
withbuilds a new record andappenda new list. - Loop statements. There is no
for,while,breakorcontinue; iteration ismap,reduceand theloopfamily. - Classes and
new. Atypenames a shape, a namespace holds its methods, and a literal builds the value. - Recursive type declarations. A
typeorchoicecannot name itself, so a tree is not a type you can declare. - Tuples. A pair is a record with two named members.
- File or JSON in the standard library.
Terminalprints, writes, inspects and reads the program’s input, andHttpsends a request. async,awaitandPromise. Work that has not run is aFuture, a value:completewaits for what one answers with andstartputs one in flight. See Asynchrony.- Floating point.
IntegerandRationalare exact:0.1::add(0.2)::is(0.3)istrue. - Calling JavaScript from Essence. An import names
.esfiles only; a.jsspecifier is aninvalid-module-specifier. JavaScript calls Essence instead, as Calling Essence from JavaScript shows. - Call-site type arguments.
identity<Integer>(1)is refused asforeign-syntax: a call writes no type arguments, and every one of them is inferred. - More than one protocol bound per parameter.
<infer T is A, is B>is a syntax error. Combine them through a protocol:protocol Both is A, is Bis the one bound. - Reading program arguments. No standard-library function reads the arguments a program was started with.