The T-Shaped Dev
ES2025 Features You Should Actually Use
Petar Ivanov
Apr 22, 2026
ES2025 Features You Should Actually Use
Source: The T-Shaped Dev · Author: Petar Ivanov · Date: 2026-04-22 · Original
ES2025 is finalized. Instead of another exhaustive listicle, Petar took the spec and tested each feature against code he'd actually written in the last year. Some genuinely improve daily work; a few are niche enough you'll forget they exist. Here's the working developer's cut — eight features worth adopting now, plus three "good to know" ones.
1. Set Methods — finally complete
Sets have been half-baked since ES6. You could create them, add items, and check membership, but if you wanted to intersect, union, or diff two sets, you wrote it yourself or reached for lodash.
Before — a hand-rolled helper you've probably copy-pasted across projects:
function intersect<T>(a: Set<T>, b: Set<T>): Set<T> {
const result = new Set<T>();
for (const item of a) {
if (b.has(item)) result.add(item);
}
return result;
}
const allowedRoles = new Set(['admin', 'editor', 'viewer']);
const userRoles = new Set(['editor', 'commenter']);
const effectiveRoles = intersect(allowedRoles, userRoles);
After:
const effectiveRoles = allowedRoles.intersection(userRoles);
// Set {'editor'}
Full list: .intersection(), .union(), .difference(), .symmetricDifference(), .isSubsetOf(), .isSupersetOf(), .isDisjointFrom().
These are immutable — they return new Sets rather than mutating the receiver, which is the right call for predictable code. If you do permissions, feature flags, or tag filtering, this cleans up real code today. TypeScript support landed in 5.5.
2. Promise.withResolvers() — externalized control
Ever needed to create a promise and resolve it from outside the executor callback? You've probably written this pattern dozens of times when bridging events into promises:
Before:
let resolve: (value: string) => void;
let reject: (reason: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));
That dangling let is ugly, and TypeScript has to be convinced resolve is assigned before use.
After:
const { promise, resolve, reject } = Promise.withResolvers<string>();
eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));
Anywhere you bridge callback or event-based code to promises — WebSocket message handlers, test utilities, manual coordination — this is cleaner. TypeScript supports it since 5.4.
3. Iterator.prototype Methods — lazy pipelines
Arrays have had .map(), .filter(), .reduce() forever. But iterators (what you get from generators, Map.keys(), Set.values()) had nothing. The common workaround was to spread them into an array first, which materializes the whole sequence into memory before you can do anything useful.
Before — wasteful: pulls every paginated user before slicing to 10:
function* generateUsers(): Generator<User> {
// yields users from a paginated API
}
const activeEmails = [...generateUsers()]
.filter(user => user.isActive)
.map(user => user.email)
.slice(0, 10);
After:
const activeEmails = generateUsers()
.filter(user => user.isActive)
.map(user => user.email)
.take(10)
.toArray();
The key difference is laziness. The new version only processes elements as they're needed: as soon as .take(10) has 10 emails, the pipeline stops and the generator never produces another user. No intermediate arrays, no wasted work — important when "the rest" of the iterator could be thousands more API calls.
Available methods: .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .forEach(), .some(), .every(), .find(). TypeScript 5.6+.
4. RegExp.escape() — the one we've waited 15 years for
If you've ever built a regex from user input, you've either pulled in a library or written a janky escape function that probably misses an edge case.
Before:
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(escapeRegex(searchTerm), 'i');
Did you remember every special character? Probably not.
After:
const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(RegExp.escape(searchTerm), 'i');
Small API, but it kills a whole class of bugs and a tiny security footgun. Use it any time a regex is built from external input — search, filtering, highlighting.
5. Float16Array — typed array for ML and graphics
A typed array for 16-bit floating-point numbers — half the memory of Float32Array.
const weights = new Float16Array([0.5, -0.3, 0.8, 0.1]);
Useful if you're doing ML inference in the browser, WebGPU work, or processing large numeric datasets where memory matters more than precision. For everyone else: you'll never touch it. The upside for the platform is it gives one less reason to drop into WASM for numeric work.
6. using / await using — automatic cleanup
If you've ever forgotten to close a file handle, release a DB connection, or clear a timer, this one is for you. Explicit Resource Management adds using and await using declarations that automatically call a cleanup hook when the variable goes out of scope — like Python's with or C#'s using.
Before — the dreaded try/finally pyramid:
async function exportReport(reportId: string) {
const connection = await db.connect();
try {
const cursor = await connection.query(`SELECT * FROM reports WHERE id = $1`, [reportId]);
try {
const file = await fs.open('./report.csv', 'w');
try {
for await (const row of cursor) {
await file.write(formatCsvRow(row));
}
} finally {
await file.close();
}
} finally {
await cursor.close();
}
} finally {
await connection.release();
}
}
After:
async function exportReport(reportId: string) {
await using connection = await db.connect();
await using cursor = await connection.query(`SELECT * FROM reports WHERE id = $1`, [reportId]);
await using file = await fs.open('./report.csv', 'w');
for await (const row of cursor) {
await file.write(formatCsvRow(row));
}
// connection, cursor, and file are all released automatically
}
The pyramid is gone. Each resource cleans itself up when the block exits — whether it returns normally or throws.
The mechanism: the resource implements [Symbol.asyncDispose]() (or [Symbol.dispose]() for sync). Node's built-in file handles already do. For your own classes:
class DatabaseConnection {
async [Symbol.asyncDispose]() {
await this.release();
}
}
V8 and Node.js 22+ ship it; Chrome has it; Firefox and Safari are catching up. TypeScript 5.2+. Server-side: use today. Client-side: wait a bit.
7. Promise.try() — safe function wrapping
You have a function that might be sync, might be async. You want to call it and always get a Promise back, with errors caught either way.
Before — verbose:
function runTask(task: () => unknown): Promise<unknown> {
try {
const result = task();
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
}
}
A common shorter version has a subtle bug — it doesn't catch synchronous throws because task() is invoked inside the .then(), but only after the surrounding function has already returned the promise:
const result = Promise.resolve().then(() => task());
Actually that does catch them — but a naive Promise.resolve(task()) does not. Either way, getting the corner cases right by hand is error-prone.
After:
const result = Promise.try(task);
One line. Catches sync throws, wraps sync return values, passes through Promises. Useful anywhere you accept a callback that could be sync or async — plugin systems, middleware chains, test runners, task queues. It's the Promise.withResolvers of error handling.
Chrome 128+, Firefox 132+, Safari 18.2+, Node 22+. TypeScript 5.7+.
8. JSON modules and import attributes
Import attributes (the with syntax) tell the runtime how an import should be interpreted:
import config from './config.json' with { type: 'json' };
import styles from './styles.css' with { type: 'css' };
Bundlers have done this for a while; now it's spec, and runtimes have caught up — Chrome, Edge, Firefox, Safari (as of April 2025) and Node.js 22+ all support with { type: 'json' }.
The important bit: type is a security mechanism, not a hint. The runtime validates the MIME type before processing the file. If someone swaps your JSON endpoint for executable code, the import fails instead of silently running it.
One caveat: JSON modules only have a default export. The whole JSON object arrives as default — no named exports.
If you're importing JSON configs, fixtures, or static data, drop fs.readFileSync / require() and use this. TypeScript 5.3+.
Other good-to-know features
Three more shipped that most developers won't reach for, but should recognize:
Error.isError()— returnstrueif a value is an Error, even across realms. If you've ever hadinstanceof Errorreturnfalsebecause the error came from a different iframe or Nodevmcontext, this fixes it.- Regex pattern modifiers
(?i:HELLO)— toggle flags likei,m,sfor one part of a regex instead of the whole pattern. Handy when only a section needs case-insensitive matching. - Duplicate named capture groups —
/(?<id>\d+)|(?<id>[a-f]+)/vis now legal. Previously a syntax error, even though only one alternative can match at a time.
TL;DR
- Set methods — replace the utility functions you've been copy-pasting for years.
Promise.withResolvers()— externalizes resolve/reject; no more danglinglet.- Iterator helpers — lazy pipelines without spreading to arrays first.
RegExp.escape()— safely escape user input for dynamic regex.using/await using— auto-cleanup for file handles, DB connections, etc. The biggest quality-of-life win in the spec.Promise.try()— wraps any function (sync or async) and guarantees a Promise with errors caught.- Import attributes (
with { type: 'json' }) — now Baseline across browsers and Node 22+. Float16Array— niche; ML/graphics only.
Minimum setup to use most of this: TypeScript 5.6+, Node.js 22+. Most features are already Baseline in browsers.
Author
Petar Ivanov
Continued reading
Keep your momentum

MKT1 Newsletter
100 B2B Startups, 100+ Stats, and 14 Graphs on Web, Social, and Content
This is Part 2 of MKT1's three-part State of B2B Marketing Report. Where Part 1 looked at teams and leadership , Part 2 turns to what marketing teams are actually doing — what their websites look like, how they use social, and what "content fuel" they're producing. Emily Kramer u
Apr 28 · 10m
Lenny's Newsletter (Lenny's Podcast)
Why Half of Product Managers Are in Trouble — Nikhyl Singhal on the AI Reinvention Threshold
Nikhyl Singhal is a serial founder and a former senior product executive at Meta, Google, and Credit Karma . Today he runs The Skip ( skip.show (https://skip.show)), a community for senior product leaders, plus offshoots like Skip Community , Skip Coach , and Skip.help . Lenny de
Apr 27 · 7m

The AI Corner
The AI Agent That Thinks Like Jensen Huang, Elon Musk, and Dario Amodei
Dominguez opens with a claim that is easy to skim past but worth stopping on: the difference between elite founders and everyone else is not raw IQ or speed — it is that each of them has internalized a repeatable mental procedure they run on every important decision. The procedur
Apr 27 · 6m