Skip to main content

On mobile? Send a link to your computer to download HTTP Toolkit there:

No spam, no newsletters - just a quick & easy download link

On mobile? Send a link to your computer to download HTTP Toolkit there:

No spam, no newsletters - just a quick & easy download link

typescript

javascript

The 5 Big Features of TypeScript 3.7 and How to Use Them

The TypeScript 3.7 release is coming soon, and it's going to be a big one.

The target release date is November 5th, and there's some seriously exciting headline features included:

  • Assert signatures
  • Recursive type aliases
  • Top-level await
  • Null coalescing
  • Optional chaining

Personally, I'm super excited about this, they're going to whisk away all sorts of annoyances that I've been fighting in TypeScript whilst building HTTP Toolkitopens in a new tab.

If you haven't been paying close attention to the TypeScript development process though, it's probably not clear what half of these mean, or why you should care. Let's talk them through.

Assert Signatures

This is a brand-new & little-known TypeScript feature, which allows you to write functions that act like type guardsopens in a new tab as a side-effect, rather than explicitly returning their boolean result.

It's easiest to demonstrate this with a JavaScript example:

Code example

Code example// In JS:

function assertString(input) {
    if (typeof input === 'string') return;
    else throw new Error('Input must be a string!');
}

function doSomething(input) {
    assertString(input);

    // ... Use input, confident that it's a string
}

doSomething('abc'); // All good
doSomething(123); // Throws an error

This pattern is neat and useful and you can't use it in TypeScript today.

TypeScript can't know that you've guaranteed the type of input after it's run assertString. Typically people just make the argument input: string to avoid this, and that's good, but that also just pushes the type checking problem somewhere else, and in cases where you just want to fail hard it's useful to have this option available.

Fortunately, soon we will:

Code example

Code example// With TS 3.7

function assertString(input: any): asserts input is string { // <-- the magic
    if (typeof input === 'string') return;
    else throw new Error('Input must be a string!');
}

function doSomething(input: string | number) {
    assertString(input);

    // input's type is just 'string' here
}

Here assert input is string means that if this function ever returns, TypeScript can narrow the type of input to string, just as if it was inside an if block with a type guard.

To make this safe, that means if the assert statement isn't true then your assert function must either throw an error or not return at all (kill the process, infinite loop, you name it).

That's the basics, but this actually lets you pull some really neat tricks:

Code example

Code example// With TS 3.7

// Asserts that input is truthy, throwing immediately if not:
function assert(input: any): asserts input { // <-- not a typo
    if (!input) throw new Error('Not a truthy value');
}

declare const x: number | string | undefined;
assert(x); // Narrows x to number | string

// Also usable with type guarding expressions!
assert(typeof x === 'string'); // Narrows x to string

// -- Or use assert in your tests: --
const a: Result | Error = doSomethingTestable();

expect(a).is.instanceOf(result); // 'instanceOf' could 'asserts a is Result'
expect(a.resultValue).to.equal(123); // a.resultValue is now legal

// -- Use as a safer ! that throws immediately if you're wrong --
function assertDefined<T>(obj: T): asserts obj is NonNullable<T> {
    if (obj === undefined || obj === null) {
        throw new Error('Must not be a nullable value');
    }
}
declare const x: string | undefined;

// Gives y just 'string' as a type, could throw elsewhere later:
const y = x!;

// Gives y 'string' as a type, or throws immediately if you're wrong:
assertDefined(x);
const z = x;

// -- Or even update types to track a function's side-effects --
type X<T extends string | {}> = { value: T };

// Use asserts to narrow types according to side effects:
function setX<T extends string | {}>(x: X<any>, v: T): asserts x is X<T> {
    x.value = v;
}

declare let x: X<any>; // x is now { value: any };

setX(x, 123);
// x is now { value: number };

This is still in flux, so don't take it as the definite result, and keep an eye on the pull requestopens in a new tab if you want the final details.

There's even discussionopens in a new tab there about allowing functions to assert something and return a type, which would let you extend the final example above to track a much wider variety of side effects, but we'll have to wait and see how that plays out.

Top-level Await

Async/awaitopens in a new tab is amazing, and makes promises dramatically cleaner to use.

Unfortunately though, you can't use them at the top level. This might not be something you care about much in a TS library or application, but if you're writing a runnable script or using TypeScript in a REPLopens in a new tab then this gets super annoying. It's even worse if you're used to frontend development, since top-level await has been working nicely in the Chrome and Firefox console for a couple of years now.

Fortunately though, a fix is coming. This is actually a general stage-3 JS proposalopens in a new tab, so it'll be everywhere else eventually too, but for TS devs 3.7 is where the magic happens.

This one's simple, but let's have another quick demo anyway:

Code example

Code example// Today:

// Your only solution right now for a script that does something async:
async function doEverything() {
    ...
    const response = await fetch('http://example.com');
    ...
}
doEverything(); // <- eugh (could use an IIFE instead, but even more eugh)

With top-level await:

Code example

Code example// With TS 3.7:

// Your script:
...
const response = await fetch('http://example.com');
...

There's a notable gotcha here: if you're not writing a script, or using a REPL, don't write this at the top level, unless you really know what you're doing!

It's totally possible to use this to write modules that do blocking async steps when imported. That can be useful for some niche cases, but people tend to assume that their import statement is a synchronous, reliable & fairly quick operation, and you could easily hose your codebase's startup time if you start blocking imports for complex async processes (even worse, processes that can fail).

This is somewhat mitigated by the semantics of imports of async modules: they're imported and run in parallel, so the importing module effectively waits for Promise.all(importedModules) before being executed. Rich Harris wrote an excellent pieceopens in a new tab on a previous version of this spec, before that change, when imports ran sequentially and this problem was much worse), which makes for good background reading on the risks here if you're interested.

It's also worth noting that this is only useful for module systems that support asynchronous imports. There isn't yet a formal spec for how TS will handle this, but that likely means that a very recent target configuration, and either ES Modules or Webpack v5 (whose alphas have experimental supportopens in a new tab) at runtime.

Recursive Type Aliases

If you're ever tried to define a recursive type in TypeScript, you may have run into StackOverflow questions like this: https://stackoverflow.com/questions/47842266/recursive-types-in-typescript.

Right now, you can't. Interfaces can be recursive, but there are limitations to their expressiveness, and type aliases can't. That means right now, you need to combine the two: define a type alias, and extract the recursive parts of the type into interfaces. It works, but it's messy, and we can do better.

As a concrete example, this is the suggestedopens in a new tab type definition for JSON data:

Code example

Code example// Today:

type JSONValue =
    | string
    | number
    | boolean
    | JSONObject
    | JSONArray;

interface JSONObject {
    [x: string]: JSONValue;
}

interface JSONArray extends Array<JSONValue> { }

That works, but the extra interfaces are only there because they're required to get around the recursion limitation.

Fixing this requires no new syntax, it just removes that restriction, so the below compiles:

Code example

Code example// With TS 3.7:

type JSONValue =
    | string
    | number
    | boolean
    | { [x: string]: JSONValue }
    | Array<JSONValue>;

Right now that fails to compile with Type alias 'JSONValue' circularly references itself. Soon though, soon...

Null Coalescing

Aside from being difficult to spell, this one is quite simple & easy. It's based on a JavaScript stage-3 proposalopens in a new tab, which means it'll also be coming to your favourite vanilla JavaScript environment too soon, if it hasn't already.

In JavaScript, there's a common pattern for handling default values, and falling back to the first valid result of a defined group. It looks something like this:

Code example

Code example// Today:

// Use the first of firstResult/secondResult which is truthy:
const result = firstResult || secondResult;

// Use configValue from provided options if truthy, or 'default' if not:
this.configValue = options.configValue || 'default';

This is useful in a host of cases, but due to some interesting quirks in JavaScript, it can catch you out. If firstResult or options.configValue can meaningfully be set to false, an empty string or 0, then this code has a bug. If those values are set, then when considered as booleans they're falsyopens in a new tab, so the fallback value (secondResult / 'default') is used anyway.

Null coalescing fixes this. Instead of the above, you'll be able to write:

Code example

Code example// With TS 3.7:

// Use the first of firstResult/secondResult which is *defined*:
const result = firstResult ?? secondResult;

// Use configSetting from provided options if *defined*, or 'default' if not:
this.configValue = options.configValue ?? 'default';

?? differs from || in that it falls through to the next value only if the first argument is null or undefined, not falsy. That fixes our bug. If you pass false as firstResult, that will be used instead of secondResult, because while it's falsy it is still defined, and that's all that's required.

Simple, but super useful, and takes a way a whole class of bugs.

Optional Chaining

Last but not least, optional chaining is another stage-3 proposalopens in a new tab which is making its way into TypeScript.

This is designed to solve an issue faced by developers in every language: how do you get data out of a data structure when some or all of it might not be present?

Right now, you might do something like this:

Code example

Code example// Today:

// To get data.key1.key2, if any level could be null/undefined:
let result = data ? (data.key1 ? data.key1.key2 : undefined) : undefined;

// Another equivalent alternative:
let result = ((data || {}).key1 || {}).key2;

Nasty! This gets much much worse if you need to go deeper, and although the 2nd example works at runtime, it won't even compile in TypeScript since the first step could be {}, in which case key1 isn't a valid key at all.

This gets still more complicated if you're trying to get into an array, or there's a function call somewhere in this process.

There's a host of other approaches to this, but they're all noisy, messy & error-prone. With optional chaining, you can do this:

Code example

Code example// With TS 3.7:

// Returns the value is it's all defined & non-null, or undefined if not.
let result = data?.key1?.key2;

// The same, through an array index or property, if possible:
array?.[0]?.['key'];

// Call a method, but only if it's defined:
obj.method?.();

// Get a property, or return 'default' if any step is not defined:
let result = data?.key1?.key2 ?? 'default';

The last case shows how neatly some of these dovetail together: null coalescing + optional chaining is a match made in heaven.

One gotcha: this will return undefined for missing values, even if they were null, e.g. in cases like (null)?.key (returns undefined). A small point, but one to watch out for if you have a lot of null in your data structures.

That's the lot! That should outline all the essentials for these features, but there's lots of smaller improvements, fixes & editor support improvements coming too, so take a look at the official roadmapopens in a new tab if you want to get into the nitty gritty.

Hope that's useful - if you've got any questions let me know on Twitteropens in a new tab.

While you're here, if you like JavaScript & want to supercharge your debugging skills, try out HTTP Toolkitopens in a new tab. One-click HTTP(S) interception & debugging for any JS page, script, or server (plus lots of other tools too).

Share this post:

Blog newsletter

Become an HTTP & debugging expert, by subscribing to receive new posts like these emailed straight to your inbox:

Related content

node.js

Automatic npm publishing, with GitHub Actions & npm granular tokens

This week, at long last, GitHub announced granular access tokens for npm. This is a big deal! It's great for security generally, but also particularly useful if you maintain any npm packages, as it removes the main downside of automating npm publishing, by allowing you to give CI jobs only a very limited token instead of full 2FA-free access to your account. In the past, I've wished for this, because I maintain a fair few npm packages including some very widely used ones. The previous solution of "just disable 2FA on your account, create an all-powerful access token with global access to every package, and give that token to your CI job" was not a comfortable one.

decentralized-web

Debugging WebRTC, IPFS & Ethereum with HTTP Toolkit

HTTP is important on the web, but as other alternative protocols grow popular in networked applications, it's often important to be able to capture, debug and mock those too. I've been working on expanding HTTP Toolkit's support for this over the past year (as one part of a project funded by EU Horizon's Next Generation Internet initiative), to extend HTTP Toolkit to cover three additional rising protocols that are often used alongside simple HTTP in decentralized web applications: WebRTC, IPFS & Ethereum.

decentralized-web

Testing libraries for the Decentralized Web

The world of decentralized web applications is an exciting place that has exploded in recent years, with technologies such as IPFS and Ethereum opening up possibilities for a peer-to-peer web - creating applications that live outside the traditional client/server model, where users to interact and control their own data directly. At the same time, it's still immature, and for software developers it lacks a lot of the affordances & ecosystem of the traditional HTTP-based web app world. There's far fewer tools and libraries for developers working in this space.