typescript-3.7:-the-5-biggest-features-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 Toolkit.

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 guards as a side-effect, rather than explicitly returning their boolean result.

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



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

function doSomething(input) {
    assertString(input);

    
}

doSomething('abc'); 
doSomething(123); 

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:



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

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

    
}

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:




function assert(input: any): asserts input { 
    if (!input) throw new Error('Not a truthy value');
}

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


assert(typeof x === 'string'); 


const a: Result | Error = doSomethingTestable();

expect(a).is.instanceOf(result); 
expect(a.resultValue).to.equal(123); 


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;


const y = x!;


assertDefined(x);
const z = x;


type X<T extends string | {}> = { value: T };


function setX<T extends string | {}>(x: X<any>, v: T): asserts x is X<T> {
    x.value = v;
}

declare let x: X<any>; 

setX(x, 123);

This is still in flux, so don’t take it as the definite result, and keep an eye on the pull request if you want the final details.

There’s even discussion 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/await 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 REPL 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 proposal, 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:




async function doEverything() {
    ...
    const response = await fetch('http://example.com');
    ...
}
doEverything(); 

With top-level await:




...
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 piece 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 support) 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 suggested type definition for JSON data:



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:



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 proposal, 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:




const result = firstResult || secondResult;


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 falsy, so the fallback value (secondResult / 'default') is used anyway.

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




const result = firstResult ?? secondResult;


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 proposal 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:




let result = data ? (data.key1 ? data.key1.key2 : undefined) : undefined;


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:




let result = data?.key1?.key2;


array?.[0]?.['key'];


obj.method?.();


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 roadmap if you want to get into the nitty gritty.

Hope that’s useful – if you’ve got any questions let me know on Twitter.

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

using-typescript-with-react

This post is based on a talk I gave at the React JS & React Native Bonn Meetup.
It aims to answer the following questions:

  • What’s TypeScript?
  • How can I use it with React?
  • Why should I (not) use it?

As this was my first public talk ever that was not at school, I’m relieved it went well – thanks to the people who attended the talk! ?
Preparing and giving presentations is a thing I really enjoy, so I guess I’ll try to do it more in the future.

On typescriptlang.org, TypeScript (TS in short) is described by the following:

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
~ typescriptlang.org

So what does this mean?
The first thing that’s obvious from this statement is that TypeScript is a typed language that’s somehow related to JavaScript.
This is the main difference to JavaScript, which is a dynamically-typed language.
Secondly, TypeScript can be compiled to plain JavaScript, which can then be executed on targets like the Browser or Node.js.
Finally, being a superset of JavaScript means that TypeScript will only add its features on top of JS while still maintaining strict compatibility with the underlying JS specification.

Let’s have a look at an example.
Since TS is a superset of JS, the following is valid code of both languages:1

function add(a, b) {
  return a   b;
}

const favNumber = add(31, 11);

As we already learned, the special thing about TypeScript is its types.
Adding types to the above code yields the following, which is now valid TypeScript, but not valid JavaScript:

function add(a: number, b: number): number {
  return a   b;
}

const favNumber: number = add(31, 11);

By adding these annotations, the TypeScript compiler can check your code to find any bugs in it.
For example, it could find mismatched parameter types or the usage of variables outside of the current scope.

Compiling TypeScript is done using the tsc CLI, which will type-check the code and – if all checks were succesful – will emit the code in plain JavaScript.
The emitted code will look a lot like your source code, just without type annotations.

TypeScript also has support for basic type-inference, which can simplify code by allowing you to omit the return type of functions and in most cases also the type of variable assignments:

function add(a: number, b: number) {
  return a   b;
}

const favNumber = add(31, 11);

(It can be hard to spot the difference: There’s no return type in line 1 and no variable type in line 5.)

Let’s have a look at the kind of types that are available in TS.
There are the primitive boolean, string, number, Symbol, null, undefined and BigInt types (yes, BigInt seems to be a primitive type in modern JS ?); there’s the void type to denote that a function does not return anything; there’s the Function type and there is the Array type which is more commonly denoted as string[] or number[].
Also, there are environment-specific types like ReactElement, HTMLInputElement or Express.App which will not be available in all projects, but are specific to your deploy targets and dependencies.

The most interesting thing about TS is that you can define your own types.
Let’s have a look at some ways how you can use that to model your domain.
There is the interface keyword which you can use to declare the shape of your objects:

interface User {
  firstName: string;
  lastName: string;
  age: number;
  state: UserState;
}

You can declare enums:

enum UserState {
  ACTIVE,
  INACTIVE,
  INVITED,
}

TypeScript even supports inheritance in its type definitions.

interface FunkyUser extends User {
  isDancing: boolean;
}

There are also more advanced types like the union type, which can be used to replace Enumerations in a more JavaScript-like way:

type UserState =
  "active" |
  "inactive" |
  "invited";

const correctState: UserState = "active"; // ✅
const incorrectState: UserState = "loggedin"; // ❌ TypeError

When type-checking, TypeScript does not check any prototype chains nor other means of inheritance, it only checks the type signatures of properties and functions your objects feature:

const bugs: User = {
  firstName: "Bugs",
  lastName: "Bunny",
  age: "too old", // TypeError: expected `number` but got `string`
  state: UserState.ACTIVE,
}

I don’t want to keep from you that you obviously can also use classes in TypeScript, but beware: Later on in this post, I will explain why I don’t think you should use them.
Anyway, here’s an example:

class JavaLover implements User {
  private firstName: string;
  private lastName: string;
  private age: number;
  private state: UserState;
  
  getOpinion() {
    return [ "!!JAVA!1!" ];
  }
}

Now that we’ve seen some basic TypeScript code and have a general understanding of what it does, let’s find out how to use it with React.

Since Babel 7 comes with built-in support for TypeScript, integrating it into your build is really simple.
I still recommend you to consult the documentation of your build tool.
Most of them include a well-written How-To-Guide concerning TypeScript setup.

Once you set up your build, you can start to use TypeScript in your components.
Here’s a simple React-Native component that receives a TodoItem and a callback in its props and displays a single Todo.

import * as React from "react";
import { View, Text, CheckBox } from "react-native";

interface TodoItem {
  id: string;
  name: string;
  isCompleted: boolean;
}

interface TodoListItemProps {
  item: TodoItem;
  onComplete: (id: string) => void;
}

function TodoListItem(props: TodoListItemProps) {
  const { item, onComplete } = props;

  return (
    <View>
      
      <Text>
        {item.name}
      Text>

      <CheckBox
        isChecked={item.isCompleted}
        onClick={state => {
          onComplete(item.id);
        }}
      />
      
    View>
  );
}

Since React is plain JavaScript at heart, it can be typed just like plain JavaScript.
You just define the schema of your component’s props using the interface declaration (see interface TodoListItemProps) and annotate the type of the component’s props param using the just-defined TodoListItemProps type.
You don’t need to specify the return type (it’s JSX.Element), since it can be inferred from the type of the component’s return expression.

Even though JSX is not part of the JavaScript spec, TypeScript is able to type-check JSX.
This allows it to validate the props you pass into components.

You can also make use of TypeScript in conjunction with React’s class API:

import * as React from "react";

interface TimerState {
  count: number;
}

class Timer extends React.Component<{}, TimerState> {
  
  state: TimerState = {
    count: 0
  }

  timerId: number | undefined = undefined;

  componentDidMount() {
    this.timerId = setInterval(
      () => {
        this.setState(
          state => ({
            count: state.count   1
          })
        );
      },
      1000
    );
  }

  componentWillUnmount() {
    if (this.timerId) {
      clearInterval(this.timerId);
    }
  }

  render() {
    return (
      <p>Count: {this.state.count}p>
    )
  }
}

When typing a class, you pass your type parameter into the React.Component you’re “extending”.
The props of the component, which are empty in the example above ({}, the empty object), come first.
The second generic type parameter is your component’s state, which simply contains a numeric count in this case.
You can also see that this uses an instance field called timerId, which is initialized as undefined.
This is why its type is number | undefined, which means “either number or undefined”.

As you see, using TypeScript with React is very straight-forward and requires no ceremony.
It’s basically a more powerful replacement for prop-types, since it supports more advanced types and can type normal JS code, too.
Also, TypeScript is validated at compile-time while prop-types does its validations during development.

If you want to play around with some code yourself, you can have a look at the example that I used in my live demo.
It is located in the todo-Folder of this repo: skn0tt/react-and-typescript-talk.

By now, I assume you have a general understanding of what TypeScript is and what it can do.
Let me go on by elaborating on (what I assume to be) false assumptions about TypeScript.

What is TypeScript not?

TypeScript is not a new way to write JavaScript – all it does is extending it by the ability to annotate your types.
First and foremost, TypeScript is a type-checker.
There seems to be a tendency to neglect this fact, as I recently read some statements that went like:

“TypeScript is great, finally our Java devs can also work on the frontend.”

While this may seem to make sense, this attitude can be harmful.
Sure: Since there are types and classes and interfaces and all that good stuff your Java developers are used to, TypeScript can seem quite inviting to them.
However, transferring your Java (or any other heavily OOP-influenced language for that matter) developers over to write TypeScript while not learning about the deep-rooted differences between Java and JavaScript can lead to big problems down the road.
Remember: Whether your JavaScript code was compiled from TypeScript or not does not change anything about its runtime behaviour.
Once it is executed, there’s not type information left – it’s plain JavaScript, in all its glory and (especially) with all its painpoints.
JavaScript uses prototype-based inheritance, is dynamically typed and has type coercion, has functional influences and many more characteristics that could not be further away from Java.
You should keep that in mind when transferring your Java devs over to TypeScript.

Another myth is that “TypeScript will eliminate all your type errors”.
While this is true for self-owned code, your (not-typescripty) dependencies may come with flawed type declarations or no type declarations at all.
This is a trade-off of TypeScript which enables easy interaction with JavaScript code, but sacrifices the virtue of true typesafeness.

How can TypeScript still help me?

Adding types to your project is a great way to document its structure.
It gives you a way to notate the shape of the data you’re passing around.
It’s also machine-readable, which means it can be enforced by a machine!
This allows for great editor support (try VS Code, it’s great!), easier refactorings and enhanced static security checks.

TypeScript not only enables you to document your code’s structure, it forces you to.
This can also cause you to stop – and reflect about the code you’re writing and wether you can create clearer structure, which will increas the quality of your code.

At least for me, having types in my code makes it easier to understand since I don’t need to dig around in the project to find out the shape of my data.
It removes a lot of cognitive complexity.

Adopting TypeScript in your projects will lead to better quality code.
By adding type information, it promotes structure and allows for advanced static analysis.
This will make it easier to read your code and also augment your workflows.

Integrating TypeScript into React projects works almost frictionless – especially, if you follow the Static Typing Guide by @piotrekwitek.

Do you have any experience with TypeScript or other languages that compile to JavaScript? If yes, what did you like/dislike about it? I’d love to hear from you in the comments or over at Twitter (@skn0tt).


If you decided on getting into TypeScript: Have fun! It will be a great time. Just try to always keep in mind that all the code you’re writing will be JavaScript at some point.