So, what’s new in ES2025?
A presentation by Christophe Porteneuve at Nordic.js 2022
A presentation by Christophe Porteneuve at Nordic.js 2022
const christophe = {
family: { wife: '👩🏻🦰 Élodie', sons: ['👦🏻 Maxence', '👦🏻 Elliott'] },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['360° ES', 'Modern Web Apps', 'Node.js', '360° Git'],
webDevSince: 1995,
mightBeKnownFor: [
'Prototype.js',
'Prototype and Script.aculo.us (“The Bungie Book”)',
'dotJS',
'Paris Web',
'NodeSchool Paris',
],
}
ECMA is an international standards body
(like ISO, IETF, W3C or WHATWG, to name a few)
ES = ECMAScript. The official standard for JavaScript*
TC39 = Technical Committee 39. Caretaker of several standards:
ECMAScript (ECMA-262), Intl
(ECMA-402), JSON (ECMA-404), etc.
Meetings every two months, mostly in the U.S.
Yearly cycle: feature freeze in January / March, formal release in June.
“ES6” = ES2015, “ES7” = ES2016, and now we say ES2022, etc.
This is all transparent and public.
Stage | Description |
---|---|
0 Strawman | “Say, it’d be nifty to get a Unicorn (🦄) operator to…” |
1 Proposal | A TC39 member becomes the proposal’s “champion.” The general shape of the API is defined, and most of the cross-cutting concerns are handled. |
2 Draft | The initial spec text is done, and covers all critical aspects and the tech semantics. |
3 Candidate | The spec is complete, duly reviewed and approved. The API is finalized and no stone is left unturned. |
4 Finished | Full Test262 coverage, 2+ shipped implementations (usually v8 and Spidermonkey), significant real-world feedback, and imprimatur by the Spec Editor. Will then be part of the next feature freeze (January-March), hence ship in the associated yearly release. |
This is a 30-minute talk.
There are currently
2 finished ES2023
proposals,
15 stage-3 proposals,
23 stage-2 proposals, and tons of stage-1.
Many of these have a lot more about them than I can illustrate in slides in under a minute…
Check out the proposals repo for full details 😉
String
#matchAll
Grabs all group matches for a sticky or global regex.
const text = 'Get in touch at tel:0983450176 or sms:478-555-1234'
text.match(/(?<protocol>[a-z]{3}):(?<number>[\d-]+)/g)
// => ['tel:0983450176', 'sms:478-555-1234'] -- 😞 WHERE THEM GROUPS AT?!
Array.from(text.matchAll(/([a-z]{3}):([\d-]+)/g)).map(
([, protocol, number]) => ({ protocol, number })
)
// => [{ number: '0983450176', protocol: 'tel' }, { number: '478-555-1234', protocol: 'sms' }]
Array.from(text.matchAll(/(?<protocol>[a-z]{3}):(?<number>[\d-]+)/g)).map((mr) => mr.groups)
// => [{ number: '0983450176', protocol: 'tel' }, { number: '478-555-1234', protocol: 'sms' }]
Promise.allSettled
/any
The two final combinators; any
short-circuits on first fulfillment.
allSettled
doesn’t short-circuit, be it on the first rejection or fulfillment: we get all
settlements for analysis.
With ES2015’s all
(short-circuits on first rejection) and race
(first settlement),
we now cover all use-cases.
// May the fastest successful fetch win!
const data = await Promise.any([fetchFromDB(), fetchFromHighSpeedLAN()])
// Run tests in parallel, but don't short-circuit!
await Promise.allSettled(tests)
// => [
// { status: 'fulfilled', value: Response… },
// { status: 'fulfilled', value: undefined },
// { status: 'rejected', reason: Error: snapshot… }
// ]
Static and instance field initializers + everything can be actually private
class APIClient extends Fetcher {
// Private instance field
#oauthToken = null
// Public static field
static VERSION = process.env.API_CLIENT_VERSION
// Public instance field
state = { authenticated: this.#oauthToken != null, loggedIn: false }
// Private instance method
#shareAuthWith(recipient) {
// Requires that `recipient` be an `APIClient`
recipient.#oauthToken = this.#oauthToken
}
}
Single-pass multi-static-field init, private static init, and more…
class Translator {
static translations = { yes: 'ja', no: 'nej', maybe: 'kanske' }
static englishWords = []
static swedishWords = []
static {
// There! We can initialize both fields with a single operation,
// instead of duplicating the computation.
for (const [english, swedish] of Object.entries(this.translations)) {
this.englishWords.push(english)
this.swedishWords.push(swedish)
}
}
}
at()
on built-in iterables 🤩
You know how Array
and String
let you use negative indices with
slice
, splice
, etc. but can't let you use them with […]
? This makes
getting the last item particularly irritating.
Well now every built-in iterable gets a .at(…)
method allowing negative
indices!
const cities = ['Stockholm', 'Göteborg', 'Malmö', 'Västerås']
cities.at(-1) // => 'Västerås'
cities.at(-2) // => 'Malmö'
Object.hasOwn()
It's long been a recommended best practice to check own-property existence with the rather long-winded code below, to circumvent ill-advised implementation overrides:
const olga = { givenName: 'Olga', familyName: 'Stern', emcee: true }
Object.prototype.hasOwnProperty.call(olga, 'emcee') // => true 😮💨
We finally have a shortcut:
Object.hasOwn(olga, 'emcee') // => true
Array
’s had find
and findIndex
for a while now (ES2015), but what
about searching from the end?
After all, we’ve long had reduceRight
and lastIndexOf
, right?
We used to have to either roll our own loops 😔 or get ham-handed with a prior (mutable!)
reverse()
, but no longer!
const codeInTheDarkLeaderboard = [
{ id: 'Bart', score: 91, firstTime: false },
{ id: 'Lisa', score: 102, firstTime: true },
{ id: 'Homer', score: 115, firstTime: true },
{ id: 'Marge', score: 138, firstTime: false },
]
const bestFirstTimer = codeInTheDarkLeaderboard.findLast(({ firstTime }) => firstTime)
// => { id: 'Homer', score: 115, firstTime: true }
const bestUsualIndex = codeInTheDarkLeaderboard.findLastIndex(({ firstTime }) => !firstTime)
// => 3
Behold! Universal JavaScript takes another step forward, with the official allowance for hashbangs at the top of source text.
#! /usr/bin/env node
if (detectCLIUse()) {
runCLI()
}
// Internals and exports here…
function runCLI() { … }
Aims to (beneficially) replace Moment, Luxon, date-fns, etc.
Immutable, nanosecond-precise, has all TZ, distinguishes absolute vs. local, explicit…
Great complement to Intl
and its formatting functions.
const meeting1 = Temporal.Date.from('2020-01-01')
const meeting2 = Temporal.Date.from('2020-04-01')
const time = Temporal.Time.from('10:00:00')
const timeZone = new Temporal.TimeZone('America/Montreal')
const absolute1 = timeZone.getAbsoluteFor(meeting1.withTime(time))
// => 2020-01-01T15:00:00.000Z
const absolute2 = timeZone.getAbsoluteFor(meeting2.withTime(time))
// => 2020-01-01T14:00:00.000Z
Check out the docs, the cookbook and Maggie’s awesome talk at dotJS 2019!
One less reason to use Lodash, eh?
const schedule = [
{ label: 'Registration & Coffee', time: '08:01', type: 'hallway' },
{ label: 'Opening', time: '09:30', type: 'stage' },
{ label: 'So, what’s new in ES2025?', time: '09:40', type: 'stage' },
{ label: 'Evolving your Design System through Data', time: '10:15', type: 'stage' },
// …
]
schedule.group(({ type }) => type)
// {
// hallway: [{ label: 'Registration…' }],
// stage: [{ label: 'Opening'… }, { label: 'So, what’s…'… }, { label: 'Evolving…'… }]
// }
schedule.groupToMap(({ type }) => type)
// => Same **as a Map** (so ANY KEY TYPE!)
Provides additional info on imports using inline syntax.
First, long-discussed use-case is allowing extra module types with an explicit type expectation to
strengthen security (sort of like HTTP’s X-Content-Type-Options: nosniff
safeguard).
// Static imports
import config from '../config/config.json' assert { type: 'json' }
// Dynamic imports
const { default: config } = await import('../config/config.json', { assert: { type: 'json' } })
The spec suggests related updates to Web Worker instantiation and HTML’s script
tag.
Allows nested classes (classes are character sets), which enables difference and intersection of classes. Very cool.
Use as a replacement for the u
(Unicode) flag when you need that capability.
// All Unicode decimal digits, except for ASCII ones:
text.match(/[\p{Decimal_Number}--[0-9]]/gv)
// All Khmer letters (so both Khmer script AND letter property)
text.match(/[\p{Script=Khmer}&&\p{Letter}]/gv)
Named capture groups (ES2018) boosted regex readability, but they erroneously forbid name re-use, even in distinct parts of an alternative 😔
Still Test262-incomplete, but a reasonsably minor update, the odds are good that it'll make the next feature freeze… if native engines implement it before then.
const year = dateText.match(/(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/)?.groups.year
A set of extra helpers for deriving arrays. The Array
API currently (2022) has 8 derivative
methods (producing a new array) vs. 9 mutative ones (altering the original array), including
reverse()
and sort()
, which most people expect not to be mutative!
const fridayMorningSpeakers = ['Colin', 'Jessy', 'Jenn', 'Charlie']
fridayMorningSpeakers.toReversed() // => ['Charlie', 'Jenn', 'Jessy', 'Colin']
fridayMorningSpeakers.toSorted() // => ['Charlie', 'Colin', 'Jenn', 'Jessy']
fridayMorningSpeakers.toSpliced(-2, 2) // => ['Jenn', 'Charlie']
fridayMorningSpeakers.with(-2, 'Creighton') // => ['Colin', 'Jessy', 'Creighton', 'Charlie']
fridayMorningSpeakers // => ['Colin', 'Jessy', 'Jenn', 'Charlie']
It's been on the table forever and is still Test262-incomplete, but is being worked on, so we'll get it eventually (also, decorators are super-popular on the TypeScript side, so the demand is clear).
So much nicer for AOP… ES provides the plumbing and the ecosystem provides operational decorators.
class SuperWidget extends Component {
@deprecate
deauth() { … }
@memoize('1m')
userFullName() { … }
@autobind
logOut() {
this.#oauthToken = null
}
@override
render() { … }
}
The groundwork for full control of sandboxed JS evaluation, tuning the global environment and available JS built-ins.
A godsend for Web-based IDEs, DOM virtualization, test frameworks, safe end-user scripting capability, server-side rendering, and more!
const realm = new ShadowRealm()
const process = await realm.importValue('./utils/processor.js', 'process')
const processedData = process(data)
// Actual isolation!
globalThis.userLocation = 'Stockholm, Sverige'
realm.evaluate('globalThis.userLocation = "Paris, France"')
globalThis.userLocation // => 'Stockholm, Sverige'
See this explainer for details.
We'll always be operating on collections and iterables in general, so we might as well beef up the standard library…
Many new Set
methods
(intersection, union, difference, disjunction, sub/superset, etc.),
built-in iterator helpers (instead of
having to write stuff like take
, filter
or map
as generative
functions), Map#emplace()
for upserting in maps, and even
collection normalization are
all at stage 2 and moving fast.
function* fibonacci() { /* … */ }
const firstTens = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
const fibs = new Set(fibonacci().take(10))
const earlyFibs = firstTens.intersection(fibonacci) // => Set { 1, 2, 3, 5, 8 }
const earlyNonFibs = firstTens.difference(fibonacci) // => Set { 4, 6, 7, 9, 10 }
const evenFibs = earlyFibs.values().filter((n) => n % 2 === 0)
const headers = new Map(undefined, {
coerceKey: (name) => name.toLowerCase()
})
headers.set('X-Requested-With', 'politeness')
headers // => Map { 'x-requested-with': 'politeness' }
IMMUTABILITY FOR THE WIN! 💪🏻
Native deeply-immutable objects (records) and arrays (tuples). Helps bring about all the benefits of immutability (e.g. referential identity), helps furthering functional JS programming.
All usual operators and APIs apply (in
, Object.keys()
, Object.is()
,
===
, etc.), and it plays nice with the standard library. Conversion from mutables is made easy
through factories. Also, JSON.parseImmutable()
!
// Records
const emma1 = #{ given: 'Emma', family: 'Bostian' }
const emma2 = #{ given: 'Emma', family: 'Twersky' }
const emma3 = #{ ...emma2, family: 'Bostian' }
emma1 === emma3 // => true!
Object.keys(emma1) // => ['family', 'given'] -- sorted!
// Tuples
#[1, 2, 3] === #[1, 2, 3] // => true!
Try the neat tutorial, the cool playground and the amazing cookbook!
Tidies up processing chains that used to be nested function calls / interpolations / arithmetic / etc.
Especially useful with unbound iterator helpers (e.g. Ramda functions). Placeholder syntax (%
)
is still subject to change.
// BEFORE 🤮
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar =>
`${envar}=${envars[envar]}`)
.join(' ')
}`,
'node',
args.join(' ')))
const result = Array.from(
take(3,
map((v) => v + 1,
filter((v) => v % 2 === 0, numbers))))
// AFTER
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%)
const result = numbers
|> filter(%, (v) => v % 2 === 0)
|> map(%, (v) => v + 1)
|> take(%, 3)
|> Array.from
Object.pick()
/ omit()
🤩 1
It's about time we didn't need Lodash for that. Very recent proposal (July 2022). Allows for key sets or
predicate callback (with optional this
specifier).
const conference = { name: 'Nordic.js', year: 2022, city: 'Stockholm', speakers: 21 }
Object.pick(conference, ['name', 'year'])
// => { name: 'Nordic.js', year: 2022 }
Object.pick(conference, (value) => typeof value === 'number')
// => { year: 2022, speakers: 21 }
Object.omit(conference, (value) => typeof value === 'number')
// => { name: 'Nordic.js', city: 'Stockholm' }
It might get even further, with syntatic sugar for key lists in picking:
conference.{name, year} // => { name: 'Nordic.js', year: 2022 }
const keys = ['name', 'city']
conference.[...keys]
// => { name: 'Nordic.js', city: 'Stockholm' }
After Perl, .NET, Ruby… JS finally gets an extended-mode regex syntax. Allows for non-significant whitespace
(including line breaks within RegExp
constructor arguments) and comments (inline or line).
Readability FTW!
const TAG_REGEX = new RegExp(String.raw`
<
# Tag name
(?<tag>[\w-]+)
\s+
# Attributes
(?<attrs>.+?)
>
# Contents
(?<content>.+?)
# Closing tag
</\k<tag>>
`, 'x')
These collections are often used with a need for capped capacity. This implies an eviction strategy: LIFO, FIFO, MRU, LRU… This is all usually implemented by hand through wrapper functions. How about having it built-in?
We may get
{FIFO
,LIFO
,MRU
,LRU
}{Map
,Set
}
if this proposal makes it through!
match
expression, which provides sort of structure-based switching. Similar features in Rust,
Python, F#, Elixir/Erlang… Just skimming the surface of what is envisioned here:
match (res) {
when ({ status: 200, body, ...rest }): handleData(body, rest)
when ({ status, destination: url }) if (300 <= status && status < 400):
handleRedirect(url)
when ({ status: 500 }) if (!this.hasRetried): do {
retry(req)
this.hasRetried = true
}
default: throwSomething()
}
const commandResult = match (command) {
when ([ 'go', dir and ('north' or 'east' or 'south' or 'west')]): go(dir);
when ([ 'take', item and /[a-z]+ ball/ and { weight }]): take(item);
default: lookAround()
}
Also short video courses:
bit.ly/screencasts-nordicjs
(50% off on all video courses for you folks!)
Christophe Porteneuve
bit.ly/nordicjs-es2025