12

All JavaScript and TypeScript features of the last 3 years

 1 year ago
source link: https://medium.com/@LinusSchlumberger/all-javascript-and-typescript-features-of-the-last-3-years-629c57e73e42
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

All JavaScript and TypeScript features of the last 3 years

1*2yo_1MdBQE1pOYWV4ueyfQ.png

TypeScript as envisioned by Stable Diffusion

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)

ECMAScript

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.
  • Symbols (previously incorrectly categorised as ES2022): 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"
}

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<string>).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 of Promise.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 existing private 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
  • 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 (non-standard, implemented in V8): 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() and String.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 to arr[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');

TypeScript

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 or unknown.
// WITHOUT:
function getFirstUnsafe(list: any[]): any {
return list[0];
}

const firstUnsafe = getFirstUnsafe(['test']); // typed as any

// WITH:
function getFirst<Type>(list: Type[]): Type {
return list[0];
}

const first = getFirst<string>(['test']); // typed as string

// In this case the parameter can even be dropped because it is inferred from the argument.
const firstInferred = getFirst(['test']); // typed as string

// The types accepted as generics can also be limited using `extends`. The Type is also usually shortened to T.
class List<T extends string | number> {
private list: T[] = [];

get(key: number): T {
return this.list[key];
}

push(value: T): void {
this.list.push(value);
}
}

const list = new List<string>();
list.push(9); // Type error: Argument of type 'number' is not assignable to parameter of type 'string'.
const booleanList = new List<boolean>(); // Type error: Type 'boolean' does not satisfy the constraint 'string | number'.

Past (Still relevant older introductions)

  • Utility Types: TypeScript contains many utility types, some of the most useful are explained here.
interface Test {
name: string;
age: number;
}

// The Partial utility type makes all properties optional.
type TestPartial = Partial<Test>; // typed as { name?: string | undefined; age?: number | undefined; }
// The Required utility type does the opposite.
type TestRequired = Required<TestPartial>; // typed as { name: string; age: number; }
// The Readonly utility type makes all properties readonly.
type TestReadonly = Readonly<Test>; // typed as { readonly name: string; readonly age: string }
// The Record utility type allows the simple definition of objects/maps/dictionaries. It is preferred to index signatures whenever possible.
const config: Record<string, boolean> = { option: false, anotherOption: true };
// The Pick utility type gets only the specified properties.
type TestLess = Pick<Test, 'name'>; // typed as { name: string; }
type TestBoth = Pick<Test, 'name' | 'age'>; // typed as { name: string; age: string; }
// The Omit utility type ignores the specified properties.type
type TestFewer = Omit<Test, 'name'>; // typed as { age: string; }
type TestNone = Omit<Test, 'name' | 'age'>; // typed as {}
// The Parameters utility type gets the parameters of a function type.
function doSmth(value: string, anotherValue: number): string {
return 'test';
}
type Params = Parameters<typeof doSmth>; // typed as [value: string, anotherValue: number]
// The ReturnType utility type gets the return type of a function type.
type Return = ReturnType<typeof doSmth>; // typed as string

// There are many more, some of which are introduced further down.
  • Conditional Types: Conditionally set a type based on if some type matches / extends another type. They can be read in the same way as the conditional (ternary) operator in JavaScript.
// Only extracts the array type if it is an array, otherwise returns the same type.
type Flatten<T> = T extends any[] ? T[number] : T;

// Extracts out the element type.
type Str = Flatten<string[]>; // typed as string

// Leaves the type alone.
type Num = Flatten<number>; // typed as number
  • Inferring with conditional types: Not all generic types need to be specified by the consumer, some can also be inferred from the code. To have conditional logic based on inferred types, the infer keyword is needed. It in a way defines temporary inferred type variables.
// Starting with the previous example, this can be written more cleanly.
type FlattenOld<T> = T extends any[] ? T[number] : T;

// Instead of indexing the array, we can just infer the Item type from the array.
type Flatten<T> = T extends (infer Item)[] ? Item : T;

// If we wanted to write a type that gets the return type of a function and otherwise is undefined, we could also infer that.
type GetReturnType<Type> = Type extends (...args: any[]) => infer Return ? Return : undefined;

type Num = GetReturnType<() => number>; // typed as number

type Str = GetReturnType<(x: string) => string>; // typed as string

type Bools = GetReturnType<(a: boolean, b: boolean) => void>; // typed as undefined
  • Tuple Optional Elements and Rest: Declare optional elements in tuples using ? and the rest based on another type using ....
// If we don't yet know how long a tuple is going to be, but it's at least one, we can specify optional types using `?`.
const list: [number, number?, boolean?] = [];
list[0] // typed as number
list[1] // typed as number | undefined
list[2] // typed as boolean | undefined
list[3] // Type error: Tuple type '[number, (number | undefined)?, (boolean | undefined)?]' of length '3' has no element at index '3'.

// We could also base the tuple on an existing type.
// If we want to pad an array at the start, we could do that using the rest operator `...`.
function padStart<T extends any[]>(arr: T, pad: string): [string, ...T] {
const [pad, ...rest] = arr;
return rest;
}

const padded = padStart([1, 2], 'test'); // typed as [string, number, number]
  • abstract Classes and methods: Classes and the methods within them can be declared as abstract to prevent them from being instantiated.
abstract class Animal {
abstract makeSound(): void;

move(): void {
console.log('roaming the earth...');
}
}

// Abstract methods need to be implemented when extended.
class Cat extends Animal {} // Compile error: Non-abstract class 'Cat' does not implement inherited abstract member 'makeSound' from class 'Animal'.

class Dog extends Animal {
makeSound() {
console.log('woof');
}
}

// Abstract classes cannot be instantiated (like Interfaces), and abstract methods cannot be called.
new Animal(); // Compile error: Cannot create an instance of an abstract class.

const dog = new Dog().makeSound(); // "woof"
  • Constructor signatures: Define the typing of constructors outside of Class declarations. Should not be used in most cases, abstract classes can be used instead.
interface MyInterface {
name: string;
}

interface ConstructsMyInterface {
new(name: string): MyInterface;
}

class Test implements MyInterface {
name: string;
constructor(name: string) {
this.name = name;
}
}

class AnotherTest {
age: number;
}

function makeObj(n: ConstructsMyInterface) {
return new n('hello!');
}

const obj = makeObj(Test); // typed as Test
const anotherObj = makeObj(AnotherTest); // Type error: Argument of type 'typeof AnotherTest' is not assignable to parameter of type 'ConstructsMyInterface'.
  • ConstructorParameters Utility Type: TypeScript helper function which gets the constructor parameters from a constructor type (but not a class).
// What if we wanted to get the constructor argument for our makeObj function.
interface MyInterface {
name: string;
}

interface ConstructsMyInterface {
new(name: string): MyInterface;
}

class Test implements MyInterface {
name: string;
constructor(name: string) {
this.name = name;
}
}

function makeObj(test: ConstructsMyInterface, ...args: ConstructorParameters<ConstructsMyInterface>) {
return new test(...args);
}

makeObj(Test); // Type error: Expected 2 arguments, but got 1.
const obj = makeObj(Test, 'test'); // typed as Test

TypeScript 4.0

  • Variadic Tuple Types: Rest elements in tuples can now be generic. The use of multiple rest elements is now also allowed.
// What if we had a function that combines two tuples of undefined length and types? How can we define the return type?

// PREVIOUSLY:
// We could write some overloads.
declare function concat(arr1: [], arr2: []): [];
declare function concat<A>(arr1: [A], arr2: []): [A];
declare function concat<A, B>(arr1: [A], arr2: [B]): [A, B];
declare function concat<A, B, C>(arr1: [A], arr2: [B, C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A], arr2: [B, C, D]): [A, B, C, D];
declare function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
declare function concat<A, B, C>(arr1: [A, B], arr2: [C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B], arr2: [C, D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B], arr2: [C, D, E]): [A, B, C, D, E];
declare function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B, C], arr2: [D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B, C], arr2: [D, E]): [A, B, C, D, E];
declare function concat<A, B, C, D, E, F>(arr1: [A, B, C], arr2: [D, E, F]): [A, B, C, D, E, F];
// Even just for three items each, this is really suboptimal.

// Instead we could combine the types.
declare function concatBetter<T, U>(arr1: T[], arr2: U[]): (T | U)[];
// But this types to (T | U)[]

// NEW:
// With variadic tuple types, we can define it easily and keep the information about the length.
declare function concatNew<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U];

const tuple = concatNew([23, 'hey', false] as [number, string, boolean], [5, 99, 20] as [number, number, number]);
console.log(tuple[0]); // 23
const element: number = tuple[1]; // Type error: Type 'string' is not assignable to type 'number'.
console.log(tuple[6]); // Type error: Tuple type '[23, "hey", false, 5, 99, 20]' of length '6' has no element at index '6'.
  • Labeled Tuple Elements: Tuple elements can now be named like [start: number, end: number]. If one of the elements is named, all of them must be named.
type Foo = [first: number, second?: string, ...rest: any[]];

// This allows the arguments to be named correctly here, it also shows up in the editor.
declare function someFunc(...args: Foo);
  • Class Property Inference from Constructors: When a property is set in the constructor, the type can now be inferred and no longer needs to be set manually.
class Animal {
// No need to set types when they are assigned in the constructor.
name;

constructor(name: string) {
this.name = name;
console.log(this.name); // typed as string
}
}
  • JSDoc @deprecated Support: The JSDoc/TSDoc @deprecated tag is now recognized by TypeScript.
/** @deprecated message */
type Test = string;

const test: Test = 'dfadsf'; // Type error: 'Test' is deprecated.

TypeScript 4.1

  • Template Literal Types: When defining literal types, types can be specified through templating like ${Type}. This allows the construction of complex string types, for example when combining multiple string literals.
type VerticalDirection = 'top' | 'bottom';
type HorizontalDirection = 'left' | 'right';
type Direction = `${VerticalDirection} ${HorizontalDirection}`;

const dir1: Direction = 'top left';
const dir2: Direction = 'left'; // Type error: Type '"left"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.
const dir3: Direction = 'left top'; // Type error: Type '"left top"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.

// This can also be combined with generics and the new utility types.
declare function makeId<T extends string, U extends string>(first: T, second: U): `${Capitalize<T>}-${Lowercase<U>}`;
  • Key Remapping in Mapped Types: Retype mapped types while still using their values like [K in keyof T as NewKeyType]: T[K].
// Let's say we wanted to reformat an object but prepend its IDs with an underscore.
const obj = { value1: 0, value2: 1, value3: 3 };
const newObj: { [Property in keyof typeof obj as `_${Property}`]: number }; // typed as { _value1: number; _value2: number; value3: number; }
  • Recursive Conditional Types: Use conditional types inside of its definition themselves. This allows for types that conditionally unpack an infinitely nested value.
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

type P1 = Awaited<string>; // typed as string
type P2 = Awaited<Promise<string>>; // typed as string
type P3 = Awaited<Promise<Promise<string>>>; // typed as string
  • Editor support for JSDOC @see tag: The JSDoc/TSDoc @see variable/type/link tag is now supported in editors.
const originalValue = 1;
/**
* Copy of another value
* @see originalValue
*/
const value = originalValue;
  • tsc — explainFiles: The --explainFiles option can be used for the TypeScript CLI to explain which files are part of the compilation and why. This can be useful for debugging. Warning: For large projects or complex setups this will generate a lot of output, instead use tsc --explainFiles | less or something similar.
tsc --explainFiles

<<output
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es5.d.ts
Library referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'
Library referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts
Library referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'
Library referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts
Library referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'
Library referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'
...
output
  • Destructured Variables Can Be Explicitly Marked as Unused: When destructuring, an underscore can be used to mark a variable as unused. This prevents TypeScript from throwing an “unused variable” error.
const [_first, second] = [3, 5];
console.log(second);

// Or even shorter
const [_, value] = [3, 5];
console.log(value);

TypeScript 4.3

  • Separate Write Types on Properties: When defining set/get accessors, the write/set type can now be different than the read/get type. This allows for setters that accept multiple formats of the same value.
class Test {
private _value: number;

get value(): number {
return this._value;
}

set value(value: number | string) {
if (typeof value === 'number') {
this._value = value;
return;
}
this._value = parseInt(value, 10);
}
}
  • override: Explicitly mark inherited class methods as overrides using override, so when the parent class changes, TypeScript can notify you that the parent method no longer exists. This allows for safer complex inheritance patterns.
class Parent {
getName(): string {
return 'name';
}
}

class NewParent {
getFirstName(): string {
return 'name';
}
}

class Test extends Parent {
override getName(): string {
return 'test';
}
}

class NewTest extends NewParent {
override getName(): string { // Type error: This member cannot have an 'override' modifier because it is not declared in the base class 'NewParent'.
return 'test';
}
}
  • static Index Signatures: When using static properties on a Class, index signatures can now also be set using static [propName: string]: string.
// PREVIOUSLY:
class Test {}

Test.test = ''; // Type error: Property 'test' does not exist on type 'typeof Test'.

// NEW:
class NewTest {
static [key: string]: string;
}

NewTest.test = '';
  • Editor Support for JSDOC @link Tags: The JSDoc/TSDoc {@link variable/type/link} inline tag is now supported and will show up and resolve in editors.
const originalValue = 1;
/**
* Copy of {@link originalValue}
*/
const value = originalValue;

TypeScript 4.4

  • Exact Optional Property Types ( — exactOptionalPropertyTypes): Using the compiler flag --exactOptionalPropertyTypes (or in tsconfig.json) assignments as undefined are no longer allowed for properties which implicitly allow undefined (for example property?: string). Instead undefined needs to explicitly be allowed like property: string | undefined.
class Test {
name?: string;
age: number | undefined;
}

const test = new Test();
test.name = 'test'; // Type error: Option 'exactOptionalPropertyTypes' cannot be specified without specifying option 'strictNullChecks'.
test.age = 0;

TypeScript 4.5

  • The Awaited Type and Promise Improvements: The new Awaited<> utility type extracts the value type from infinitely nested Promises (like await does for the value). This also improved the type inference for Promise.all().
// Let's say we want to have a generic awaited value.
// We can use the Awaited utility type for this (its source code was part of a previous example),
// so infinitely nested Promises all resolve to their value.
type P1 = Awaited<string>; // typed as string
type P2 = Awaited<Promise<string>>; // typed as string
type P3 = Awaited<Promise<Promise<string>>>; // typed as string
  • type Modifiers on Import Names: Inside normal (not import type) import statements, the type keyword can be used to signal that the value should only be imported for type compilation (and can be stripped away).
// PREVIOUSLY:
// The optimal way to import types is to use the `import type` keyword to prevent them from actually being imported after compilation.
import { something } from './file';
import type { SomeType } from './file';
// This needs two import statements for the same file.

// NEW:
// Now this can be combined into one statement.
import { something, type SomeType } from './file';
  • const Assertions: When defining constants as const can be used to accurately type them as literal types. This has a lot of use cases and makes accurate typings easier. It also makes objects and arrays readonly, which prevents mutations of constant objects.
// PREVIOUSLY:
const obj = { name: 'foo', value: 9, toggle: false }; // typed as { name: string; value: number; toggle: boolean; }
// Any value can be assigned because they are generally typed.
obj.name = 'bar';

const tuple = ['name', 4, true]; // typed as (string | number | boolean)[]
// The length and exact type can not be determined from the type. Any values can be assigned anywhere.
tuple[0] = 0;
tuple[3] = 0;

// NEW:
const objNew = { name: 'foo', value: 9, toggle: false } as const; // typed as { readonly name: "foo"; readonly value: 9; readonly toggle: false; }
// No value can be assigned (because it is defined as "foo" (and also is readonly)).
objNew.name = 'bar'; // type error: Cannot assign to 'name' because it is a read-only property.

const tupleNew = ['name', 4, true] as const; // typed as readonly ["name", 4, true]
// The length and exact type are now defined and nothing can be assigned (because it is defined as literals (and also is readonly)).
tupleNew[0] = 0; // type error: Cannot assign to '0' because it is a read-only property.
tupleNew[3] = 0; // type error: Index signature in type 'readonly ["name", 4, true]' only permits reading.
  • Snippet Completions for Methods in Classes: When a class inherits method types, they are now suggested as snippets in editors.
1*31No189vLt2Kdx5Ay_Ihig.gif

TypeScript 4.6

  • Indexed Access Inference Improvements When directly indexing a Type with a key, the type will now be more accurate when it’s on the same object. Also, just a good example to show what is possible with modern TypeScript.
interface AllowedTypes {
'number': number;
'string': string;
'boolean': boolean;
}

// The Record specifies the kind and value type from the allowed types.
type UnionRecord<AllowedKeys extends keyof AllowedTypes> = { [Key in AllowedKeys]:
{
kind: Key;
value: AllowedTypes[Key];
logValue: (value: AllowedTypes[Key]) => void;
}
}[AllowedKeys];

// The function logValue only accepts the value of the Record.
function processRecord<Key extends keyof AllowedTypes>(record: UnionRecord<Key>) {
record.logValue(record.value);
}

processRecord({
kind: 'string',
value: 'hello!',
// The value used to implicitly have the type string | number | boolean,
// but now is correctly inferred to just string.
logValue: value => {
console.log(value.toUpperCase());
}
});
  • TypeScript Trace Analyzer ( — generateTrace): The --generateTrace <Output folder> option can be used for the TypeScript CLI to generate a file containing details regarding the type checking and compilation process. This can help optimize complex types.
tsc --generateTrace trace

cat trace/trace.json
<<output
[
{"name":"process_name","args":{"name":"tsc"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},
{"name":"thread_name","args":{"name":"Main"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},
{"name":"TracingStartedInBrowser","cat":"disabled-by-default-devtools.timeline","ph":"M","ts":...,"pid":1,"tid":1},
{"pid":1,"tid":1,"ph":"B","cat":"program","ts":...,"name":"createProgram","args":{"configFilePath":"/...","rootDir":"/..."}},
{"pid":1,"tid":1,"ph":"B","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},
{"pid":1,"tid":1,"ph":"E","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},
{"pid":1,"tid":1,"ph":"X","cat":"program","ts":...,"name":"resolveModuleNamesWorker","dur":...,"args":{"containingFileName":"/..."}},
...
output

cat trace/types.json
<<output
[{"id":1,"intrinsicName":"any","recursionId":0,"flags":["..."]},
{"id":2,"intrinsicName":"any","recursionId":1,"flags":["..."]},
{"id":3,"intrinsicName":"any","recursionId":2,"flags":["..."]},
{"id":4,"intrinsicName":"error","recursionId":3,"flags":["..."]},
{"id":5,"intrinsicName":"unresolved","recursionId":4,"flags":["..."]},
{"id":6,"intrinsicName":"any","recursionId":5,"flags":["..."]},
{"id":7,"intrinsicName":"intrinsic","recursionId":6,"flags":["..."]},
{"id":8,"intrinsicName":"unknown","recursionId":7,"flags":["..."]},
{"id":9,"intrinsicName":"unknown","recursionId":8,"flags":["..."]},
{"id":10,"intrinsicName":"undefined","recursionId":9,"flags":["..."]},
{"id":11,"intrinsicName":"undefined","recursionId":10,"flags":["..."]},
{"id":12,"intrinsicName":"null","recursionId":11,"flags":["..."]},
{"id":13,"intrinsicName":"string","recursionId":12,"flags":["..."]},
...
output

TypeScript 4.7

  • ECMAScript Module Support in Node.js: When using ES Modules instead of CommonJS, TypeScript now supports specifying the default. Specify it in the tsconfig.json.
...
"compilerOptions": [
...
"module": "es2020"
]
...
  • type in package.json: The field type in package.json can be set to "module", which is needed to use node.js with ES Modules. In most cases, this is enough for TypeScript and the compiler option above is not needed.
...
"type": "module"
...
  • Instantiation Expressions: Instantiation expressions allow the specifying of type parameters when referencing a value. This allows the narrowing of generic types without creating wrappers.
class List<T> {
private list: T[] = [];

get(key: number): T {
return this.list[key];
}

push(value: T): void {
this.list.push(value);
}
}

function makeList<T>(items: T[]): List<T> {
const list = new List<T>();
items.forEach(item => list.push(item));
return list;
}

// Let's say we want to have a function that creates a list but only allows certain values.
// PREVIOUSLY:
// We need to manually define a wrapper function and pass the argument.
function makeStringList(text: string[]) {
return makeList(text);
}

// NEW:
// Using instantiation expressions, this is much easier.
const makeNumberList = makeList<number>;
  • extends Constraints on infer Type Variables: When inferring type variables in conditional types, they can now directly be narrowed/constrained by using extends.
// Let's say we want to type a type that only gets the first element of an array if it's a string.
// We can use conditional types for this.

// PREVIOUSLY:
type FirstIfStringOld<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;

// But this needs two nested conditional types. We can also do it in one.
type FirstIfString<T> =
T extends [string, ...unknown[]]
// Grab the first type out of `T`
? T[0]
: never;

// This is still suboptimal because we need to index the array for the correct type.

// NEW:
// Using extends Constraints on infer Type Variables, this can be declared a lot easier.
type FirstIfStringNew<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;
// Note that the typing worked the same before, this is just a cleaner syntax.

type A = FirstIfStringNew<[string, number, number]>; // typed as string
type B = FirstIfStringNew<["hello", number, number]>; // typed as "hello"
type C = FirstIfStringNew<["hello" | "world", boolean]>; // typed as "hello" | "world"
type D = FirstIfStringNew<[boolean, number, string]>; // typed as never
  • Optional Variance Annotations for Type Parameters: Generics can have different behaviors when checking if they “match”, for example, the allowing of inheritance is reversed for getters and setters. This can now be optionally specified for clarity.
// Let's say we have an interface / a class that extends another one.
interface Animal {
animalStuff: any;
}

interface Dog extends Animal {
dogStuff: any;
}

// And we have some generic "getter" and "setter".
type Getter<T> = () => T;

type Setter<T> = (value: T) => void;

// If we want to find out if Getter<T1> matches Getter<T2> or Setter<T1> matches Setter<T2>, this depends on the covariance.
function useAnimalGetter(getter: Getter<Animal>) {
getter();
}

// Now we can pass a Getter into the function.
useAnimalGetter((() => ({ animalStuff: 0 }) as Animal));
// This obviously works.

// But what if we want to use a Getter which returns a Dog instead?
useAnimalGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// This works as well because a Dog is also an Animal.

function useDogGetter(getter: Getter<Dog>) {
getter();
}

// If we try the same for the useDogGetter function we will not get the same behavior.
useDogGetter((() => ({ animalStuff: 0 }) as Animal)); // Type error: Property 'dogStuff' is missing in type 'Animal' but required in type 'Dog'.
// This does not work, because a Dog is expected, not just an Animal.

useDogGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// This, however, works.

// Intuitively we would maybe expect the Setters to behave the same, but they don't.
function setAnimalSetter(setter: Setter<Animal>, value: Animal) {
setter(value);
}

// If we pass a Setter of the same type it still works.
setAnimalSetter((value: Animal) => {}, { animalStuff: 0 });

function setDogSetter(setter: Setter<Dog>, value: Dog) {
setter(value);
}

// Same here.
setDogSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });

// But if we pass a Dog Setter into the setAnimalSetter function, the behavior is reversed from the Getters.
setAnimalSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 }); // Type error: Argument of type '(value: Dog) => void' is not assignable to parameter of type 'Setter<Animal>'.

// This time it works the other way around.
setDogSetter((value: Animal) => {}, { animalStuff: 0, dogStuff: 0 });

// NEW:
// To signal this to TypeScript (not needed but helpful for readability), use the new Optional Variance Annotations for Type Parameters.
type GetterNew<out T> = () => T;
type SetterNew<in T> = (value: T) => void;
  • Resolution Customization with moduleSuffixes: When using environments that have custom file suffixes (for example .ios for native app builds), these can now be specified for TypeScript to correctly resolve imports. Specify them in the tsconfig.json.
...
"compilerOptions": [
...
"module": [".ios", ".native", ""]
]
...
import * as foo from './foo';
// This first checks ./foo.ios.ts, ./foo.native.ts, and finally ./foo.ts.
  • Go to Source Definition in editors: In editors, the new “go to source definition” menu option is available. It is similar to “go to definition”, but prefers .ts and .js files over type definitions (.d.ts).
1*y45nF8mb-nfBuVIUPt7KJQ.gif
1*qvzGqfF844cXCPMeDrg_IQ.gif

TypeScript 4.9

  • The satisfies Operator: The satisfies operator allows checking the compatibility with types without actually assigning that type. This allows for keeping more accurate inferred types while still keeping compatibility.
// PREVIOUSLY:
// Let's say we have an object/map/dictionary which stores various items and their colors.
const obj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} // typed as { fireTruck: number[]; bush: string; ocean: number[]; }

// This implicitly types the properties so we can operate on the arrays and the string.
const rgb1 = obj.fireTruck[0]; // typed as number
const hex = obj.bush; // typed as string

// Let's say we only want to allow certain objects.
// We could use a Record type.
const oldObj: Record<string, [number, number, number] | string> = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} // typed as Record<string, [number, number, number] | string>
// But now we lose the typings of the properties.
const oldRgb1 = oldObj.fireTruck[0]; // typed as string | number
const oldHex = oldObj.bush; // typed as string | number

// NEW:
// With the satisfies keyword we can check compatibility with a type without actually assigning it.
const newObj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} satisfies Record<string, [number, number, number] | string> // typed as { fireTruck: [number, number, number]; bush: string; ocean: [number, number, number]; }
// And we still have the typings of the properties, the array even got more accurate by becoming a tuple.
const newRgb1 = newObj.fireTruck[0]; // typed as number
const newRgb4 = newObj.fireTruck[3]; // Type error: Tuple type '[number, number, number]' of length '3' has no element at index '3'.
const newHex = newObj.bush; // typed as string
  • “Remove Unused Imports” and “Sort Imports” Commands for Editors: In editors, the new commands (and auto-fixes) “Remove Unused Imports” and “Sort Imports” make managing imports easier.
1*z5SG6BKwzyxrG6sG7OKciw.gif

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK