Lets Talk about ...
Whats new (and upcoming) in JS
2019 Edition
Stages
- Stage 0: Strawperson (Nothing/PoC)
- Stage 1: Proposal (Polyfill)
- Stage 2: Draft (Experimental)
- Stage 3: Candidate (Spec compliant)
- Stage 4: Finished (Shipping)
Stage 0 (Strawperson)
Nested Import
Currently
import { strictEqual } from 'assert';
import { check as checkC } from './client.js';
import { check as checkS } from './server.js';
describe('fancy feature #5', () => {
it('should work on the client', () => {
strictEqual(checkC(), 'client ok');
});
it('should work on the server', () => {
strictEqual(checkS(), 'server ok');
});
});
With nested imports
describe('fancy feature #5', () => {
import { strictEqual } from 'assert';
it('should work on the client', () => {
import { check } from './client.js';
strictEqual(check(), 'client ok');
});
it('should work on the server', () => {
import { check } from './server.js';
strictEqual(check(), 'server ok');
});
});
Advantages
- Lazy evaluation
- Scoped to enclosing block
- Backwards compatible
Stage 1 (Proposal)
Math
Extensions
Seeded (Pseudo) Random Numbers
Generates an enumerable based on the passed seed
for (const [i,r] of enumerate(
Math.seededPRNG({seed:0})
)) {
// do something with each value
if(i >= limit) break;
}
Clamp
Makes sure value stays in given range
Math.clamp(x, lower, upper);
Math.clamp(-10, 0, 100); // → 0
Math.clamp(1000, 0, 100); // → 100
Math.clamp(0, 10, 100); // → 10
Scale
Maps value from input range to output range
Math.scale(x, inLow, inHigh, outLow, outHigh);
Math.scale(0.25, 0, 1, 0, 100); // → 25
Math.scale(0.25, 0, 1, -180, 180); // → -90
Iterator
Extensions
Sample Iterator
function* naturals() {
let i = 0;
while (true) {
yield i;
i += 1;
}
}
for (const i of naturals) {
// do something with each value
if(i >= limit) break;
}
Filter
Returns iterator which skips some values of source iterator
const evens = naturals()
.filter((n) => n % 2 === 0);
for (const i of evens) {
// do something with each value
if(i >= limit) break;
}
Other extensions
- map
- count
- min / max
- find
- skip / take
Observable
Mix of Promise and Iterator
Create Observable
let observable = new Observable(observer => {
const {next, error, complete } = observer;
// Emit a single value after 1 second
let timer = setTimeout(() => {
next('hello');
complete();
}, 1000);
// Return cleanup function
return () => clearTimeout(timer);
});
Observable.prototype.subscribe
let subscription = observable.subscribe(
x => console.log(x),
err => console.log(`Failed: ${err}`),
() => console.log('Finished')
);
Observable.prototype.forEach
observable.forEach(x => {
console.log(`Received value: ${ x }`);
}).then(() => {
console.log('Finished successfully')
}).catch(err => {
console.log(`Finished with error: ${ err }`);
})
Observable.prototype.filter
observable.filter(callback)
Observable.of(1, 2, 3).filter(value => {
return value > 2;
}).subscribe(value => {
console.log(value);
});
// → 3
Observable.prototype.map
Observable.of(1, 2, 3).map(value => {
return value * 2;
}).subscribe(value => {
console.log(value);
});
// → 2 → 4 → 6
Observable.prototype.reduce
Observable.of(1, 2, 3).reduce((prev, curr) => {
return prev + curr;
}).subscribe(result => {
console.log(result);
});
// → 6
Observable.prototype.concat
Observable.of(1, 2, 3).concat(
Observable.of(4, 5, 6),
Observable.of(7, 8, 9)
).subscribe(result => {
console.log(result);
});
// → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
Promise
Extensions
try / attempt
- Converts function to promise
- and sync errors to async
current
promise = new Promise((resolve, reject) => {
throw new Error('i failed');
});
promise.catch(console.log); // → i failed
Promise.try
promise = Promise.try(() => {
throw new Error('i failed');
});
promise.catch(console.log); // → i failed
promise = Promise.try(() => {
return 'result';
});
promise.then(console.log); // → result
Promise.all
(ES 2015)
- returns all results as array
- short-circuits on reject
Promise.race
(ES 2015)
- resolves first settled promise
- rejects if first settled promise rejects
Promise.any
- rejects when no promise resolves
- short-circuits on resolve
Promise.allSettled
- returns all results as array
- does not short circuit
Stage 2 (Draft)
Decorators
Temporal
(like native MomentJS)
Classes
Instant // Point in time
OffsetDateTime // Offset from UTC (+01:00)
CivilDateTime // Offset from Calendar
ZonedDateTime // Offset + Timezone (Europe/Rome)
(Weak)Set
Extensions
Array-like
- filter
- map
- find
- reduce
- join
- ...
Manipulating
- addAll(... elements)
- deleteAll(... elements)
- intersection
- union
- difference
- ...
Comparing
- isSubsetOf
- isDisjointFrom
- isSupersetOf
(Weak)Map
Extensions
Array-like
- of / from
- filter
- mapKeys / mapValues
- reduce
- join
- ...
Manipulating
Aggregation
throw
in expression contexts
Parameter initializers
function save(filename =
throw new TypeError('Argument required')
) {}
Arrow function bodies
lint(ast, {
with: () => throw new Error(
"avoid using 'with' statements.
")
});
Conditional expressions
const obj = str
? JSON.parse(str)
: throw new Error('String required')
;
Logical operations
class Product {
get id() { return this._id; }
set id(value) {
this._id = value ||
throw new Error("Invalid value")
;
}
}
Function implementation hiding
Function.prototype.toString
Error.prototype.stack
function foo() {
"hide implementation";
// ...
}
Realms
Intuitions
- sandbox
- iframe without DOM
- principled version of Node's 'vm' module
- sync worker
Use cases
- security isolation
- plugins
- in browser code editors
- server-side rendering
- testing
Example
let g = window; // outer global
let r = new Realm(); // root realm
let f = r.evaluate("(function() { return 17 })");
f() === 17 // true
Error.prototype.stack
String
Extensions
String.replaceAll
const query = 'q=query+string+parameters';
const withSpaces = query.replace(/\+/g, ' ');
const withSpaces = query.replaceAll('+', ' ');
Optional
Chaining
Current
var query = 'input[name=foo]';
var $input = $form.querySelector(query)
var value = $input ? $input.value : undefined
With optional chaining
var fooValue = $form.querySelector(query)?.value
var street = user?.address?.street
iterator.return?.() // manually close an iterator
Nullish
Coalescing
current
const undefinedValue =
response.settings?.undefinedValue
|| 'some other default'
; // result: 'some other default'
const nullValue =
response.settings?.nullValue
|| 'some other default'
; // result: 'some other default'
current problems
response.settings?.showSplashScreen || true
// False evaluates to false, result: true
response.settings?.animationDuration || 300;
// 0 evaluates to false, result: 300
response.settings?.headerText || 'Hello, world!'
// '' evaluates to false, result: 'Hello, world!'
Nullish Coalescing
response.settings?.showSplashScreen ?? true
response.settings?.animationDuration ?? 300;
response.settings?.headerText ?? 'Hello, world!'
Stage 3 (Candidate)
Numeric
Separators
current
1000000000
101475938.38
// Is this a billion? a hundred millions? Ten millions?
let fee = 12300;
// is this 12,300? Or 123, because it's in cents?
let amount = 1234500;
// is this 1,234,500? Or cents, hence 12,345?
// Or financial, 4-fixed 123.45?
Numeric Separators
1_000_000_000 // Ah, so a billion
101_475_938.38 // hundreds of millions
let fee = 123_00; // $123 (12300 cents)
let fee = 12_300; // $12,300 (thats much)
let amount = 12345_00; // 12,345 (1234500 cents)
let amount = 123_4500; // 123.45 (4-fixed fin.)
let amount = 1_234_500; // 1,234,500
Numeric Separators
0.000_001 // fractions
1e10_000 // exponents
let nibbles = 0b1010_0001_1000_0101; // binary
let brandColor = 0x44_BB_44; // hex (colors)
Class
Extensions
private
in classes or objects
class ColorFinder {
static #red = "#ff0000";
static #blue = "#00ff00";
static #green = "#0000ff";
class ColorFinder {
static #red = "#ff0000";
static #blue = "#00ff00";
static #green = "#0000ff";
static colorName(name) {
switch (name) {
case "red": return ColorFinder.#red;
case "blue": return ColorFinder.#blue;
case "green": return ColorFinder.#green;
default: throw new RangeError("unknown color");
}
}
}
Stage 4 (Shipping)
Optional catch binding
try { ... } catch (unused) { ... }
try { ... } catch { ... }
Intl
Internationalization aka I18l
Langauge dependent formatting for
- Dates / Times
- Numbers
- Plural Rules
- Collator (comparing)
Intl.DateTimeFormat
const DateFormat = Intl.DateTimeFormat;
new DateFormat('en-US').format(date);
// → '12/20/2019'
new DateFormat('en-GB').format(date);
// → '20/12/2019'
// Include a fallback language
new DateFormat(['foo', 'de']).format(date)
// → '20.12.2019'
Intl.NumberFormat
new Intl.NumberFormat('de-DE', {
style: 'currency', currency: 'EUR'
}).format(number); // → '123.456,79 €'
// the Japanese yen doesn't use a minor unit
new Intl.NumberFormat('ja-JP', {
style: 'currency', currency: 'JPY'
}).format(number); // → '¥123,457'
new Intl.NumberFormat('en-IN', {
maximumSignificantDigits: 3
}).format(number); // → '1,23,000'
Intl.PluralRules
new Intl.PluralRules('en-US').select(0);
// → 'other'
new Intl.PluralRules('en-US').select(1);
// → 'one'
new Intl.PluralRules('en-US').select(2);
// → 'other'
Intl.Collator
function letterSort(lang, letters) {
letters.sort(new Intl.Collator(lang).compare);
return letters;
}
console.log(letterSort('de', ['a','z','ä']));
// expected output: Array ["a", "ä", "z"]
console.log(letterSort('sv', ['a','z','ä']));
// expected output: Array ["a", "z", "ä"]
Array
Extensions
Array.prototype.flat
const array = [1, [2, [3, [4]]]];
array.flat(); // → [1, 2, [3, [4]]];
array.flat(2); // → [1, 2, 3, [4]];
array.flat(Infinity); // → [1, 2, 3, 4];
Array.prototype.flatMap
const dup = (x) => [x, x];
[3, 4].map(dup); // → [[3, 3], [4, 4]]
[3, 4].map(dup).flat(); // 🐌 → [3, 3, 4, 4]
[3, 4].flatMap(dup); // 🚀 → [3, 3, 4, 4]
Object
Extensions
Object.prototype.entities
const object = { x: 42, y: 50 };
const entries = Object.entries(object);
// → [['x', 42], ['y', 50]]
Object.prototype.fromEntities
const result = Object.fromEntries(entries);
// → { x: 42, y: 50 }

Thats it
Whats next?
Questions?