Tomasz Swistak

|
|

TypeScript 3.0. What Has Come?

Nearly two years after the release of version 2.0 and two months from the latest 2.9, Microsoft has released the newest version of TypeScript – the language we use in our projects. Let’s see what changes have been made and how they can affect our daily work.

In TypeScript 3.0 we can identify just five changes affecting the you can use the language. Only one of those is flagged as a breaking change. But that’s enough introduction, let’s see what’s new!

RICHER TUPLE TYPES

Tuples in TypeScript are in fact JavaScript strongly-typed arrays (at the single element level) with constant length. At least, that’s the way it used to be, and you can see an example in Listing 1.

Listing 1 Tuples in TypeScript

type Triple = [string, number, boolean];
const a: Triple = ['a', 2, false];

Starting from TypeScript 3.0 we can define tuples with an undefined maximal number of elements, needing just a minimum. An example is given in Listing 2.

Listing 2 Tuple with undefined maximal number of elements

type StringAndNumbers = [string, ...number[]];
const b1: StringAndNumbers = ['a', 1, 2, 3];
const b2: StringAndNumbers = ['a'];

The first element has a string type, which is mandatory, but the rest, which is just numbers, can be omitted. What’s more, we can even define the tuple without a lower limit (in fact it’s just an array) or an empty tuple. See Listing 3.

Listing 3 Empty tuple and without lower limit

type Strings = [...string];
type Empty = [];
const c1: Strings = ['a', 'b'];
const c2: Strings = [];
const c3: Empty = [];
const c4: Empty = ['a', 'b']; // invalid type

Such tuples have practical uses. For example, we can force an array to have at least one element on the types level. Another use in a more complex context is shown in the next chapter.

EXTRACTING AND SPREADING FUNCTION ARGUMENTS WITH TUPLE TYPES

As you may know, in JavaScript we can use arguments provided to the function, even when we haven’t named any in the code, by using “arguments” variable. Of course, this isn’t allowed in TypeScript, where we have to tell the compiler everything. The difference is shown in Listing 4.

Listing 4 Example of using arguments which aren’t defined in a function’s definition

// JavaScript
function jsHello() {
  return [...arguments].join(',');
}

// JavaScript, second way
function jsHello1(...args) {
  return args.join(',');
}

// TypeScript
function tsHello(...args: any[]): string {
  return args.join(',');
}

This is fine for the compiler, but when we’re using TypeScript we don’t want to use the “any” type. Let’s say we want to have only strings and numbers provided. Then, we can of course change “any” to “(string | number)”. But this way we are telling the compiler “I want to have an array which has strings or numbers.” We don’t restrict which element is a string, which is a number, and how many we should have. Let’s say we know that the first argument is a string, the following two are numbers, and the rest are strings. From TypeScript 3.0 we can define it using tuple types as shown in Listing 5.

Listing 5 Implicitly defined types for each argument in an arguments array

function hello(...args: [string, number, number, ...string[]]): string {
    return args.join(',');
}

How about a different use case? Let’s see Listing 6.

Listing 6 Function for composing two given functions and its usage

function compose2<T1 extends any[], T1R, T2>(f1: (...args1: T1) => T1R,  f2: (arg: T1R) => T2) {
  return (...a: T1) => f2(f1(...a));
}

const add = (x: number, y: number) => x + y;
const sqr = (x: number) => x * x;
const addAndSqr = compose2(add, sqr);

addAndSqr(1, 2); // valid
addAndSqr('a', 2); // invalid type

TypeScript 3.0 allows us to do a generic type “T1 extends any[]” which in fact creates a tuple type. Here we tell the compiler that T1 is the type of all of the first function’s arguments. It can be anything, but we will have it strongly typed. As you can see in the last line of listing 6, strict type checking works and tells us that we can’t use a string in place of a number. This example shows that we will be able to use such typings in functional programming. Maybe we will get better typings for libraries like Ramda in the future?

“UNKNOWN”

As you may know, TypeScript has an “any” type which can handle literally everything – it reverts JavaScript’s weak typing for the specific variable. It’s a bad practice to overuse it, but due to JavaScript’s nature sometimes we need it. One case we want to use “any” is when we don’t know the type, but we will check what it is and later use it strongly typed. With “any” we wouldn’t even need to check the type – we could use a variable just like that without compiler errors. From TypeScript 3.0 we can use the “unknown” type. It tells the compiler “We don’t know what we have here, so we can’t use it until we know what it is.” The difference is shown in Listing 7.

Listing 7 Differences between any and unknown

const a1: any = 'a';
const a2: unknown = 'a';
a1.length; // = 1
a2.length; // compiler error
a1(); // error during execution
a2(); // compiler error
new a1(); // error during execution
new a2(); // compiler error
const a3 = a1 + 3; // = 'a3'
const a4 = a2 + 3; // compiler error
const b: unknown = { a: 'b' };
console.log(b.a); // compiler error
function hasA(obj: any): obj is { a: any } {
    return !!obj
        && typeof obj === 'object'
        && 'a' in obj;
}
if (hasA(b)) {
    console.log(b.a); // = 'b'
}

It’s worth noting that this is a breaking change, because from version 3.0 “unknown” has become a language’s keyword, so it can’t be used as a name anymore.

SUPPORT FOR DEFAULTPROPS

Another novelty in TypeScript 3.0 is language-level support for React’s defaultProps. For those of you who haven’t heard of defaultProps, I’ll describe them briefly. Normally, in JavaScript (ECMAScript 6), if we would like to provide the default React component’s parameters, we would do it using static object defaultProps as shown in Listing 8.

Listing 8 Using defaultProps in JavaScript

import * as React from 'react';
import * as ReactDOM from 'react-dom';

class Heading extends React.Component {
  render() {
    return <h1>{this.props.title.toUpperCase()}</h1>
  }
}

Heading.defaultProps = { title: 'Hello!' };
const elem = document.querySelector('#target');
ReactDOM.render(<Heading />, elem);

As we can see, thanks to the usage of defaultProps we don’t need to perform null checks on title, because we are sure that it always has some value. In the past, TypeScript couldn’t understand this and we had to do a null check. In Listings 9 and 10 I’ve shown how we could approach this issue in TypeScript 2.x, and how we can do it now.

Listing 9 TypeScript 2.x way of using defaultProps

type Props = {
  title?: string;
};

class Heading extends React.Component<Props> {
  static defaultProps = {
    title: 'Hello!'
  };
  render() {
    const title = this.props.title;
    return <h1>{title && title.toUpperCase()}</h1>
  }    
}

Listing 10 TypeScript 3.0 way of using defaultProps

type Props = {
  title?: string;
};

class Heading extends React.Component<Props> {
  static defaultProps = {
    title: 'Hello!'
  };
  render() {
    const title = this.props.title;
    return <h1>{title.toUpperCase()}</h1>
  }    
}

As you can see, the code from the newest TypeScript version doesn’t need the null check, because the compiler can see the parameter value defined in defaultProps.

PROJECT REFERENCES

Probably the biggest new feature of TypeScript 3.0. From now on, we will be able to define cross‑project references. Thanks to this change, we will be able to use some new project architecture scenarios like:

  • Using shared code for client and server projects. Compiled output would also share the code, not have a separate copy of each shared code file.
  • Unit tests which don’t contain own copy of the main project’s files.
  • Monorepos (many projects dependent on each other), along with the use of battle-tested solutions like Lerna and Yarn Workspaces

That’s all thanks to new entries in tsconfig: “composite” flag for compiler options and “references.” Also, we have a new parameter for tsc (TypeScript’s compiler): –build which can build whole TypeScript projects in accordance with a given tsconfig.

As an example, let’s consider the following project structure:

The client’s project tsconfig.json is provided in Listing 11. It can be built using the command: “tsc -b composite/client”. To use a shared project we simply imported the things we need from it without any additional structures in the code.

Listing 11 tsconfig.json with composite flag and reference on shared project

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["es2015", "dom"],
    "outDir": "../../lib/client",
    "composite": true,
    "strict": true,
    "esModuleInterop": true
  },
  "references": [
    {
      "path": "../shared"
    }
  ]
}

After compiling both client and server we get the following structure:

As you can see, the “shared” directory is in fact shared between the client and the server. In older versions, we would get the “shared” directory copied to both client and server, thus creating an output structure different from the source structure.

For information on how to use project references with Lerna, I can recommend this GitHub repository.

IN CONCLUSION…

As we have seen, there weren’t a lot of changes. Mostly they concerned a typing system to provide static types for many more JavaScript cases. The biggest change is, of course, project references, but for now we can’t tell how widely it will be used. We are eager to see how much value it will provide to both existing and new projects!

Do you need more technical insights? Check out my other articles!

This post was also published on ITNEXT.