This article goes through almost all of the changes of the last 3 years (and some from earlier) in JavaScript / ECMAScript and TypeScript.
Not all of the following features will be relevant to you or even practical, but they should instead serve to show what’s possible and to deepen your understanding of these languages.
There are a lot of TypeScript features I left out because they can be summarized as “This didn’t work like you would expect it to but now does”. So if something didn’t work in the past, try it again now.
Overview
- JavaScript / ECMAScript (oldest first)
- TypeScript (oldest first)
Past (Still relevant older introductions)
- Tagged template literals: By prepending a function name in front of a template literal, the function will be passed the parts of the template literals and the template values. This has some interesting uses.
// Let's say we want to write a way to log arbitrary strings containing a number, but format the number.
// We can use tagged templates for that.
function formatNumbers(strings: TemplateStringsArray, number: number): string {
return strings[0] + number.toFixed(2) + strings[1];
}
console.log(formatNumbers`This is the value: ${0}, it's important.`); // This is the value: 0.00, it's important.// Or if we wanted to "translate" (change to lowercase here) translation keys within strings.
function translateKey(key: string): string {
return key.toLocaleLowerCase();
}
function translate(strings: TemplateStringsArray, ...expressions: string[]): string {
return strings.reduce((accumulator, currentValue, index) => accumulator + currentValue + translateKey(expressions[index] ?? ''), '');
}
console.log(translate`Hello, this is ${'NAME'} to say ${'MESSAGE'}.`); // Hello, this is name to say message.
ES2020
- Optional chaining: To access a value (via indexing) of a potentially undefined object, optional chaining can be used by using
?
after the parent object name. This is also possible to use for indexing ([...]
) or function calling.
// PREVIOUSLY:
// If we have an object variable (or any other structure) we don't know for certain is defined,
// We can not easily access the property.
const object: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const value = object.name; // type error: 'object' is possibly 'undefined'.// We could first check if it is defined, but this hurts readability and gets complex for nested objects.
const objectOld: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueOld = objectOld ? objectOld.name : undefined;
// NEW:
// Instead we can use optional chaining.
const objectNew: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueNew = objectNew?.name;
// This can also be used for indexing and functions.
const array: string[] | undefined = Math.random() > 0.5 ? undefined : ['test'];
const item = array?.[0];
const func: (() => string) | undefined = Math.random() > 0.5 ? undefined : () => 'test';
const result = func?.();
- import(): Dynamically import, just like
import ... from ...
, but at runtime and using variables.
let importModule;
if (shouldImport) {
importModule = await import('./module.mjs');
}
- String.matchAll: Get multiple matches of a regular expression including their capture groups, without using a loop.
const stringVar = 'testhello,testagain,';// PREVIOUSLY:
// Only gets matches, but not their capture groups.
console.log(stringVar.match(/test([w]+?),/g)); // ["testhello,", "testagain,"]
// Only gets one match, including its capture groups.
const singleMatch = stringVar.match(/test([w]+?),/);
if (singleMatch) {
console.log(singleMatch[0]); // "testhello,"
console.log(singleMatch[1]); // "hello"
}
// Gets the same result, but is very unintuitive (the exec method saves the last index).
// Needs to be defined outside the loop (to save the state) and be global (/g),
// otherwise this will produce an infinite loop.
const regex = /test([w]+?),/g;
let execMatch;
while ((execMatch = regex.exec(stringVar)) !== null) {
console.log(execMatch[0]); // "testhello,", "testagain,"
console.log(execMatch[1]); // "hello", "again"
}
// NEW:
// Regex needs to be global (/g), also doesn't make any sense otherwise.
const matchesIterator = stringVar.matchAll(/test([w]+?),/g);
// Needs to be iterated or converted to an array (Array.from()), no direct indexing.
for (const match of matchesIterator) {
console.log(match[0]); // "testhello,", "testagain,"
console.log(match[1]); // "hello", "again"
}
- Promise.allSettled(): Like
Promise.all()
, but waits for all Promises to finish and does not return on the first reject/throw. It makes handling all errors easier.
async function success1() {return 'a'}
async function success2() {return 'b'}
async function fail1() {throw 'fail 1'}
async function fail2() {throw 'fail 2'}// PREVIOUSLY:
console.log(await Promise.all([success1(), success2()])); // ["a", "b"]
// but:
try {
await Promise.all([success1(), success2(), fail1(), fail2()]);
} catch (e) {
console.log(e); // "fail 1"
}
// Notice: We only catch one error and can't access the success values.
// PREVIOUS FIX (really suboptimal):
console.log(await Promise.all([ // ["a", "b", undefined, undefined]
success1().catch(e => { console.log(e); }),
success2().catch(e => { console.log(e); }),
fail1().catch(e => { console.log(e); }), // "fail 1"
fail2().catch(e => { console.log(e); })])); // "fail 2"
// NEW:
const results = await Promise.allSettled([success1(), success2(), fail1(), fail2()]);
const sucessfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult).value);
console.log(sucessfulResults); // ["a", "b"]
results.filter(result => result.status === 'rejected').forEach(error => {
console.log((error as PromiseRejectedResult).reason); // "fail 1", "fail 2"
});
// OR:
for (const result of results) {
if (result.status === 'fulfilled') {
console.log(result.value); // "a", "b"
} else if (result.status === 'rejected') {
console.log(result.reason); // "fail 1", "fail 2"
}
}
- globalThis: Access variables in the global context, regardless of the environment (browser, NodeJS, …). Still considered bad practice, but sometimes necessary. Akin to
this
at the top level in the browser.
console.log(globalThis.Math); // Math Object
- import.meta: When using ES-modules, get the current module URL
import.meta.url
.
console.log(import.meta.url); // "file://..."
- export * as … from …: Easily re-export defaults as submodules.
export * as am from 'another-module'
import { am } from 'module'
ES2021
- String.replaceAll(): Replace all instances of a substring in a string, instead of always using a regular expression with the global flag (/g).
const testString = 'hello/greetings everyone/everybody';
// PREVIOUSLY:
// Only replaces the first instance
console.log(testString.replace('/', '|')); // 'hello|greetings everyone/everybody'// Instead a regex needed to be used, which is worse for performance and needs escaping.
// Not the global flag (/g).
console.log(testString.replace(///g, '|')); // 'hello|greetings everyone|everybody'
// NEW:
// Using replaceAll this is much clearer and faster.
console.log(testString.replaceAll('/', '|')); // 'hello|greetings everyone|everybody'
- Promise.any: When only one result of a list of promises is needed, it returns the first result, it only rejects when all promises reject and returns an
AggregateError
, instead ofPromise.race
, which instantly rejects.
async function success1() {return 'a'}
async function success2() {return 'b'}
async function fail1() {throw 'fail 1'}
async function fail2() {throw 'fail 2'}// PREVIOUSLY:
console.log(await Promise.race([success1(), success2()])); // "a"
// but:
try {
await Promise.race([fail1(), fail2(), success1(), success2()]);
} catch (e) {
console.log(e); // "fail 1"
}
// Notice: We only catch one error and can't access the success value.
// PREVIOUS FIX (really suboptimal):
console.log(await Promise.race([ // "a"
fail1().catch(e => { console.log(e); }), // "fail 1"
fail2().catch(e => { console.log(e); }), // "fail 2"
success1().catch(e => { console.log(e); }),
success2().catch(e => { console.log(e); })]));
// NEW:
console.log(await Promise.any([fail1(), fail2(), success1(), success2()])); // "a"
// And it only rejects when all promises reject and returns an AggregateError containing all the errors.
try {
await Promise.any([fail1(), fail2()]);
} catch (e) {
console.log(e); // [AggregateError: All promises were rejected]
console.log(e.errors); // ["fail 1", "fail 2"]
}
- Nullish coalescing assignment (??=): Only assign a value when it was “nullish” before (null or undefined).
let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';// Assigns the new value to x1, because undefined is nullish.
x1 ??= 'b';
console.log(x1) // "b"
// Does not assign a new value to x2, because a string is not nullish.
// Also note: getNewValue() is never executed.
x2 ??= getNewValue();
console.log(x1) // "a"
- Logical and assignment (&&=): Only assign a value when it was “truthy” before (true or a value that converts to true).
let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';// Does not assign a new value to x1, because undefined is not truthy.
// Also note: getNewValue() is never executed.
x1 &&= getNewValue();
console.log(x1) // undefined
// Assigns a new value to x2, because a string is truthy.
x2 &&= 'b';
console.log(x1) // "b"
- Logical or assignment (||=): Only assign a value when it was “falsy” before (false or converts to false).
let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';// Assigns the new value to x1, because undefined is falsy.
x1 ||= 'b';
console.log(x1) // "b"
// Does not assign a new value to x2, because a string is not falsy.
// Also note: getNewValue() is never executed.
x2 ||= getNewValue();
console.log(x1) // "a"
- WeakRef: Hold a “weak” reference to an object, without preventing the object from being garbage-collected.
const ref = new WeakRef(element);// Get the value, if the object/element still exists and was not garbage-collected.
const value = ref.deref;
console.log(value); // undefined
// Looks like the object does not exist anymore.
- Numeric literal separators (_): Separate numbers using
_
for better readability. This does not affect functionality.
const int = 1_000_000_000;
const float = 1_000_000_000.999_999_999;
const max = 9_223_372_036_854_775_807n;
const binary = 0b1011_0101_0101;
const octal = 0o1234_5670;
const hex = 0xD0_E0_F0;
ES2022
- #private: Make class members (properties and methods) private by naming them starting with
#
. These then can only be accessed from the class itself. They can not be deleted or dynamically assigned. Any incorrect behavior will result in a JavaScript (not TypeScript) syntax error. This is not recommended for TypeScript projects, instead just use the existingprivate
keyword.
class ClassWithPrivateField {
#privateField;
#anotherPrivateField = 4; constructor() {
this.#privateField = 42; // Valid
this.#privateField; // Syntax error
this.#undeclaredField = 444; // Syntax error
console.log(this.#anotherPrivateField); // 4
}
}
const instance = new ClassWithPrivateField();
instance.#privateField === 42; // Syntax error
- Symbols: Unique keys for objects:
Symbol("foo") === Symbol("foo"); // false
. Used internally.
const obj: { [index: string]: string } = {};const symbolA = Symbol('a');
const symbolB = Symbol.for('b');
console.log(symbolA.description); // "a"
obj[symbolA] = 'a';
obj[symbolB] = 'b';
obj['c'] = 'c';
obj.d = 'd';
console.log(obj[symbolA]); // "a"
console.log(obj[symbolB]); // "b"
// The key cannot be accessed with any other symbols or without a symbol.
console.log(obj[Symbol('a')]); // undefined
console.log(obj['a']); // undefined
// The keys are not enumerated when using for ... in.
for (const i in obj) {
console.log(i); // "c", "d"
}
- static class members: Mark any class fields (properties and methods) as static.
class Logger {
static id = 'Logger1';
static type = 'GenericLogger';
static log(message: string | Error) {
console.log(message);
}
}class ErrorLogger extends Logger {
static type = 'ErrorLogger';
static qualifiedType;
static log(e: Error) {
return super.log(e.toString());
}
}
console.log(Logger.type); // "GenericLogger"
Logger.log('Test'); // "Test"
// The instantiation of static-only classes is useless and only done here for demonstration purposes.
const log = new Logger();
ErrorLogger.log(new Error('Test')); // Error: "Test" (not affected by instantiation of the parent)
console.log(ErrorLogger.type); // "ErrorLogger"
console.log(ErrorLogger.qualifiedType); // undefined
console.log(ErrorLogger.id); // "Logger1"
// This throws because log() is not an instance method but a static method.
console.log(log.log()); // log.log is not a function
- static initialization blocks in classes: Block which is run when a class is initialized, basically the “constructor” for static members.
class Test {
static staticProperty1 = 'Property 1';
static staticProperty2;
static {
this.staticProperty2 = 'Property 2';
}
}console.log(Test.staticProperty1); // "Property 1"
console.log(Test.staticProperty2); // "Property 2"
- Import Assertions: Assert which type an import is using
import ... from ... assert { type: 'json' }
. Can be used to directly import JSON without having to parse it.
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
- RegExp match indices: Get the start and end indexes of regular expression matches and capture groups. This works for
RegExp.exec()
,String.match()
andString.matchAll()
.
const matchObj = /(test+)(hello+)/d.exec('start-testesthello-stop');// PREVIOUSLY:
console.log(matchObj?.index);
// NEW:
if (matchObj) {
// Start and end index of entire match (before we only had the start).
console.log(matchObj.indices[0]); // [9, 18]
// Start and end indexes of capture groups.
console.log(matchObj.indices[1]); // [9, 13]
console.log(matchObj.indices[2]); // [13, 18]
}
- Negative indexing (.at(-1)): When indexing an array or a string,
at
can be used to index from the end. It’s equivalent toarr[arr.length - 1)
console.log([4, 5].at(-1)) // 5
- hasOwn: Recommended new way to find out which properties an object has instead of using
obj.hasOwnProperty()
. It works better for some edge cases.
const obj = { name: 'test' };console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'gender')); // false
- Error cause: An optional cause can now be specified for Errors, which allows specifying of the original error when re-throwing it.
try {
try {
connectToDatabase();
} catch (err) {
throw new Error('Connecting to database failed.', { cause: err });
}
} catch (err) {
console.log(err.cause); // ReferenceError: connectToDatabase is not defined
}
Future (can already be used with TypeScript 4.9)
- Auto-Accessor: Automatically make a property private and create get/set accessors for it.
class Person {
accessor name: string; constructor(name: string) {
this.name = name;
console.log(this.name) // 'test'
}
}
const person = new Person('test');
Basics (Context for further introductions)
- Generics: Pass through types to other types. This allows for types to be generalized but still typesafe. Always prefer this over using
any
orunknown
.
// WITHOUT:
function getFirstUnsafe(list: any[]): any {
return list[0];
}co