So, what’s new in ES2025?

A presentation by Christophe Porteneuve at Nordic.js 2022

@porteneuve

whoami


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',
  ],
}
          

ES2025?!

ECMA, TC39, ECMAScript and JavaScript

ECMA and TC39

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.

* Which happens to be, in the U.S., a trademark of Oracle Corp. Yeah, I know 🤢
This is why the community periodically discusses renaming the language to “JS” (and also to avoid Java HR confusion)

The way the language evolves at TC39

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.

The 5 stages of the TC39 process

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.

A disclaimer

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…

This is a curated list 🤗

Check out the proposals repo for full details 😉

Quick recap: ES2020–2022

A curated list of stuff too many people missed 😉

ES2020: 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' }]
          

ES2020 / ES2021: 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… }
            // ]
          

ES2022: Class fields

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
              }
            }
          

ES2022: Static blocks

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)
                }
              }
            }
          
h/t Dr. Axel Rauschmayer (@rauschma / 2ality.net) for the example

ES2022: 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ö'
          

ES2022: 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
          

ES2023

(guaranteed or likely)

ES2023: Find from last 😙

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
          

ES2023: Hashbang grammar

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() { … }
          

ES2023: Temporal 🥳 3

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!

ES2023: Array grouping 🎉 3

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!)
          

ES2023: Import assertions + JSON modules 3

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.

ES2023: RegExp v flag 3

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)
          

ES2023: Duplicate named capture groups 3

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
          

🔮 Looking beyond… 🔮

Change array by copy 3

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']
          

Decorators 3

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() { … }
            }
          

Shadow Realms 3

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.

Tons of collection / iterator stuff 2

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' }
          

Records & Tuples 2

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!

Pipeline operator 2

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' }
          

Extended-mode regular expressions 🎉 1

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')
          
This was last presented a year ago (Oct 21). You can also use inline comments in regular expression literals. It would be lovely to get regular expression syntax coloration in constructor arguments using a VSCode extension and custom tagged template literal. 😉

Policy maps and sets 😎 1

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!

(Again, a July 2022 proposal, but this is not super heavy and should get there before 2025 😉)

Pattern matching 🤯 1

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()
            }
          
Last presented March 2022 (as of October 2022).

Do you like amazing courses?


Check out our trainings!


Also short video courses:

bit.ly/screencasts-nordicjs

(50% off on all video courses for you folks!)

Tack!

Satsa alltid på JS.


Christophe Porteneuve

@porteneuve

Slides are at bit.ly/nordicjs-es2025