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.
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.`);
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'}.`);
- 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);
obj[symbolA] = 'a';
obj[symbolB] = 'b';
obj['c'] = 'c';
obj.d = 'd';
console.log(obj[symbolA]);
console.log(obj[symbolB]);
console.log(obj[Symbol('a')]);
console.log(obj['a']);
for (const i in obj) {
console.log(i);
}
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.
const object: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const value = object.name;
const objectOld: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueOld = objectOld ? objectOld.name : undefined;
const objectNew: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueNew = objectNew?.name;
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?.();
- Nullish coalescing operator (??): Instead of using the
||
operator for conditionally assigning, the new??
operator can be used. Instead of applying to all falsy values, it only applies toundefined
andnull
.
const value: string | undefined = Math.random() > 0.5 ? undefined : 'test';
const anotherValue = value || 'hello';
console.log(anotherValue);
const incorrectValue = '' || 'incorrect';
console.log(incorrectValue);
const anotherIncorrectValue = 0 || 'incorrect';
console.log(anotherIncorrectValue);
const newValue = value ?? 'hello';
console.log(newValue)
const correctValue = '' ?? 'incorrect';
console.log(correctValue);
const anotherCorrectValue = 0 ?? 'incorrect';
console.log(anotherCorrectValue);
- 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,';
console.log(stringVar.match(/test([\w]+?),/g));
const singleMatch = stringVar.match(/test([\w]+?),/);
if (singleMatch) {
console.log(singleMatch[0]);
console.log(singleMatch[1]);
}
const regex = /test([\w]+?),/g;
let execMatch;
while ((execMatch = regex.exec(stringVar)) !== null) {
console.log(execMatch[0]);
console.log(execMatch[1]);
}
const matchesIterator = stringVar.matchAll(/test([\w]+?),/g);
for (const match of matchesIterator) {
console.log(match[0]);
console.log(match[1]);
}
- 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'}
console.log(await Promise.all([success1(), success2()]));
try {
await Promise.all([success1(), success2(), fail1(), fail2()]);
} catch (e) {
console.log(e);
}
console.log(await Promise.all([
success1().catch(e => { console.log(e); }),
success2().catch(e => { console.log(e); }),
fail1().catch(e => { console.log(e); }),
fail2().catch(e => { console.log(e); })]));
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);
results.filter(result => result.status === 'rejected').forEach(error => {
console.log((error as PromiseRejectedResult).reason);
});
for (const result of results) {
if (result.status === 'fulfilled') {
console.log(result.value);
} else if (result.status === 'rejected') {
console.log(result.reason);
}
}
- 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);
- import.meta: When using ES-modules, get the current module URL
import.meta.url
.
console.log(import.meta.url);
- 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';
console.log(testString.replace('/', '|'));
console.log(testString.replace(/\//g, '|'));
console.log(testString.replaceAll('/', '|'));
- 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'}
console.log(await Promise.race([success1(), success2()]));
try {
await Promise.race([fail1(), fail2(), success1(), success2()]);
} catch (e) {
console.log(e);
}
console.log(await Promise.race([
fail1().catch(e => { console.log(e); }),
fail2().catch(e => { console.log(e); }),
success1().catch(e => { console.log(e); }),
success2().catch(e => { console.log(e); })]));
console.log(await Promise.any([fail1(), fail2(), success1(), success2()]));
try {
await Promise.any([fail1(), fail2()]);
} catch (e) {
console.log(e);
console.log(e.errors);
}
- Nullish coalescing assignment (??=): Only assign a value when it was “nullish” before (null or undefined).
let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';
x1 ??= 'b';
console.log(x1)
x2 ??= getNewValue();
console.log(x1)
- 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';
x1 &&= getNewValue();
console.log(x1)
x2 &&= 'b';
console.log(x1)
- 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';
x1 ||= 'b';
console.log(x1)
x2 ||= getNewValue();
console.log(x1)
- WeakRef: Hold a “weak” reference to an object, without preventing the object from being garbage-collected.
const ref = new WeakRef(element);
const value = ref.deref;
console.log(value);
- 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
- Top level await: The
await
keyword can now be used at the top level of ES modules, eliminating the need for a wrapper function and improving error handling.
async function asyncFuncSuccess() {
return 'test';
}
async function asyncFuncFail() {
throw new Error('Test');
}
try {
(async () => {
console.log(await asyncFuncSuccess());
try {
await asyncFuncFail();
} catch (e) {
console.error(e);
throw e;
}
})();
} catch (e) {
console.error(e);
}
console.log('Hey');
console.log(await asyncFuncSuccess());
try {
await asyncFuncFail();
} catch (e) {
console.error(e);
}
console.log('Hello');
- #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;
this.#privateField;
this.#undeclaredField = 444;
console.log(this.#anotherPrivateField);
}
}
const instance = new ClassWithPrivateField();
instance.#privateField === 42;
- 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);
Logger.log('Test');
const log = new Logger();
ErrorLogger.log(new Error('Test'));
console.log(ErrorLogger.type);
console.log(ErrorLogger.qualifiedType);
console.log(ErrorLogger.id);
console.log(log.log());
- 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);
console.log(Test.staticProperty2);
- 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);
- 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');
console.log(matchObj?.index);
if (matchObj) {
console.log(matchObj.indices[0]);
console.log(matchObj.indices[1]);
console.log(matchObj.indices[2]);
}
- 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])
for getting a value (but not setting).
console.log([4, 5].at(-1)) const array = [4, 5];
array.at(-1) = 3;
- 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'));
console.log(Object.hasOwn(obj, 'gender'));
- 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);
}
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)
}
}
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
orunknown
.
function getFirstUnsafe(list: any[]): any {
return list[0];
}const firstUnsafe = getFirstUnsafe(['test']);
function getFirst<Type>(list: Type[]): Type {
return list[0];
}
const first = getFirst<string>(['test']);
const firstInferred = getFirst(['test']);
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);
const booleanList = new List<boolean>();
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;
}
type TestPartial = Partial<Test>;
type TestRequired = Required<TestPartial>;
type TestReadonly = Readonly<Test>;
const config: Record<string, boolean> = { option: false, anotherOption: true };
type TestLess = Pick<Test, 'name'>;
type TestBoth = Pick<Test, 'name' | 'age'>;
type TestFewer = Omit<Test, 'name'>;
type TestNone = Omit<Test, 'name' | 'age'>;
function doSmth(value: string, anotherValue: number): string {
return 'test';
}
type Params = Parameters<typeof doSmth>;
type Return = ReturnType<typeof doSmth>;
- 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.
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>;
type Num = Flatten<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.
type FlattenOld<T> = T extends any[] ? T[number] : T;
type Flatten<T> = T extends (infer Item)[] ? Item : T;
type GetReturnType<Type> = Type extends (...args: any[]) => infer Return ? Return : undefined;
type Num = GetReturnType<() => number>;
type Str = GetReturnType<(x: string) => string>;
type Bools = GetReturnType<(a: boolean, b: boolean) => void>;
- Tuple Optional Elements and Rest: Declare optional elements in tuples using
?
and the rest based on another type using...
.
const list: [number, number?, boolean?] = [];
list[0]
list[1]
list[2]
list[3]
function padStart<T extends any[]>(arr: T, pad: string): [string, ...T] {
return [pad, ...arr];
}
const padded = padStart([1, 2], 'test');
- 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...');
}
}
class Cat extends Animal {}
class Dog extends Animal {
makeSound() {
console.log('woof');
}
}
new Animal();
const dog = new Dog().makeSound();
- 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);
const anotherObj = makeObj(AnotherTest);
- ConstructorParameters Utility Type: TypeScript helper function which gets the constructor parameters from a constructor type (but not a class).
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);
const obj = makeObj(Test, '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.
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];
declare function concatBetter<T, U>(arr1: T[], arr2: U[]): (T | U)[];
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]);
const element: number = tuple[1];
console.log(tuple[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[]];
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 {
name; constructor(name: string) {
this.name = name;
console.log(this.name);
}
}
- JSDoc @deprecated Support: The JSDoc/TSDoc
@deprecated
tag is now recognized by TypeScript.
type Test = string;const test: Test = 'dfadsf';
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';
const dir3: Direction = 'left top';
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]
.
const obj = { value1: 0, value2: 1, value3: 3 };
const newObj: { [Property in keyof typeof obj as `_${Property}`]: 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>;
type P2 = Awaited<Promise<string>>;
type P3 = Awaited<Promise<Promise<string>>>;
- Editor support for JSDOC @see tag: The JSDoc/TSDoc
@see variable/type/link
tag is now supported in editors.
const originalValue = 1;
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 usetsc --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);
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 {
return 'test';
}
}
- static Index Signatures: When using static properties on a Class, index signatures can now also be set using
static [propName: string]: string
.
class Test {}Test.test = '';
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;
const value = originalValue;
TypeScript 4.4
- Exact Optional Property Types (--exactOptionalPropertyTypes): Using the compiler flag
--exactOptionalPropertyTypes
(or intsconfig.json
) assignments asundefined
are no longer allowed for properties which implicitly allowundefined
(for exampleproperty?: string
). Insteadundefined
needs to explicitly be allowed likeproperty: string | undefined
.
class Test {
name?: string;
age: number | undefined;
}const test = new Test();
test.name = undefined;
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 (likeawait
does for the value). This also improved the type inference forPromise.all()
.
type P1 = Awaited<string>;
type P2 = Awaited<Promise<string>>;
type P3 = Awaited<Promise<Promise<string>>>;
- type Modifiers on Import Names: Inside normal (not
import type
) import statements, thetype
keyword can be used to signal that the value should only be imported for type compilation (and can be stripped away).
import { something } from './file';
import type { SomeType } from './file';
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 arraysreadonly
, which prevents mutations of constant objects.
const obj = { name: 'foo', value: 9, toggle: false };
obj.name = 'bar';const tuple = ['name', 4, true];
tuple[0] = 0;
tuple[3] = 0;
const objNew = { name: 'foo', value: 9, toggle: false } as const;
objNew.name = 'bar';
const tupleNew = ['name', 4, true] as const;
tupleNew[0] = 0;
tupleNew[3] = 0;
- Snippet Completions for Methods in Classes: When a class inherits method types, they are now suggested as snippets in editors.
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;
}
type UnionRecord<AllowedKeys extends keyof AllowedTypes> = { [Key in AllowedKeys]:
{
kind: Key;
value: AllowedTypes[Key];
logValue: (value: AllowedTypes[Key]) => void;
}
}[AllowedKeys];
function processRecord<Key extends keyof AllowedTypes>(record: UnionRecord<Key>) {
record.logValue(record.value);
}
processRecord({
kind: 'string',
value: 'hello!',
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 tracecat 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
inpackage.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;
}
function makeStringList(text: string[]) {
return makeList(text);
}
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
.
type FirstIfStringOld<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;
type FirstIfString<T> =
T extends [string, ...unknown[]]
? T[0]
: never;
type FirstIfStringNew<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;
type A = FirstIfStringNew<[string, number, number]>;
type B = FirstIfStringNew<["hello", number, number]>;
type C = FirstIfStringNew<["hello" | "world", boolean]>;
type D = FirstIfStringNew<[boolean, number, string]>;
- 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.
interface Animal {
animalStuff: any;
}interface Dog extends Animal {
dogStuff: any;
}
type Getter<T> = () => T;
type Setter<T> = (value: T) => void;
function useAnimalGetter(getter: Getter<Animal>) {
getter();
}
useAnimalGetter((() => ({ animalStuff: 0 }) as Animal));
useAnimalGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
function useDogGetter(getter: Getter<Dog>) {
getter();
}
useDogGetter((() => ({ animalStuff: 0 }) as Animal));
useDogGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
function setAnimalSetter(setter: Setter<Animal>, value: Animal) {
setter(value);
}
setAnimalSetter((value: Animal) => {}, { animalStuff: 0 });
function setDogSetter(setter: Setter<Dog>, value: Dog) {
setter(value);
}
setDogSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });
setAnimalSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });
setDogSetter((value: Animal) => {}, { animalStuff: 0, dogStuff: 0 });
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 thetsconfig.json
.
...
"compilerOptions": [
...
"module": [".ios", ".native", ""]
]
...
import * as foo from './foo';
- 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
).
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.
const obj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
}
const rgb1 = obj.fireTruck[0];
const hex = obj.bush;
const oldObj: Record<string, [number, number, number] | string> = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
}
const oldRgb1 = oldObj.fireTruck[0];
const oldHex = oldObj.bush;
const newObj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} satisfies Record<string, [number, number, number] | string>
const newRgb1 = newObj.fireTruck[0];
const newRgb4 = newObj.fireTruck[3];
const newHex = newObj.bush;
- “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.