/ typescript

Advanced TypeScript for Java Developers

I'm thrilled to present my first TypeScript related post on the Vavr blog — also featuring a video.

When I designed the Vavr library four years ago, my goal was to mimic in Java existing functional API's known from Scala. My experience is that the results are decent. However, the weakest point is Java's type system.

I don't want to go into detail here about Java's type system. In fact, TypeScript is much more interesting on the type level. To recap, TypeScript is one of the most popular (strongly-typed) JavaScript alternatives (counting worldwide interest on Google Search over the last 5 years):

javascript-alternatives-1

js-alternatives-chart-1

TypeScript is a superset of JavaScript. Namely, every valid JavaScript program is also a valid TypeScript program. TypeScript adds types to — and stays in line with JavaScript while JavaScript evolves. This gives me the impression that TypeScript could exist for a very long time.

TS ⊃ JS
Off-topic: I made an interesting observation
about the hexagon aspect ratio while drawing
the image above:
    sqrt(3)/2 * width   = height
    sqrt(3)/2 * 3,14... = 2,72
    sqrt(3)/2 * π       ≈ ℇ
😅

Let's gently dive into TypeScript...

JavaScript isn't untyped, nor it is stringly-typed — it is dynamically-typed. Some days ago I stumbled upon this code snippet on the web that underlines the dynamic nature of JavaScript:

dynamic typing

The first moment I did not know whether to laugh or to cry. In JavaScript, it is perfectly ok that a method may return a union of unrelated types.

The following JavaScript implementation would cover all statements when running the program above.

// JavaScript

let isSad = true;
let toggle = true;

function sad() {
    if (isSad && toggle) {
        toggle = !toggle;
        return true;
     } else {
         return {
             stop: () => isSad = false
        };
    }
}

function beAwesome() {}

When calling sad() we observe the following:

// = true
sad();

// = { stop(): function }
sad();

As a programmer, I want to ensure that I can rely on my functions. That's why I mainly use functions that always return the same output given a certain input.

The main issue of the sad() function is that it internally uses mutable state in an unpredictable way. However, we are also able to provoke a similar behavior using Java. It is the developer's responsibility to write high-quality code, not the responsibility of the language.

Personally, I have the feeling that JavaScript allows me to write 'better' programs than in Java because it is a functional language. But without static typing it is only half the fun. That's why I use TypeScript.

In TypeScript we are able to make the types explicit at compile time.

// TypeScript

interface ISad {
    stop: () => void;
}

// inferred type is boolean here
let isSad = true;
let toggle = true;

function sad(): boolean | ISad {
    if (isSad && toggle) {
        toggle = !toggle;
        return true;
     } else {
         return {
             stop: () => isSad = false
        };
    }
}

function beAwesome() {}

Now we get a nice compile error.

// life motto
if (sad() === true) {
    // 💥 property 'stop' does not exist
    //    on type 'boolean | ISad'
    sad().stop();
    beAwesome();
}

Quintessence: it is not up to me to judge the life motto — but one thing for sure, the code example is questionable. 💩

Andvanced type system features

Syntax-wise, Java and TypeScript are very similar, there are classes, interfaces, generics and so on.

interface IMap<K, V> {
    get(key: K): V | undefined;
}

TypeScript has several interesting features that don't exist in Java, like

  • structural types: Given a function function f<T extends { s: string }>(t: T) {}, we can use any object that matches the function parameter type structure, e.g. f({ s: "", n: 0 })
  • type aliases and union types: type Primitive = string | number | boolean — we defined Primitive to be an alias for the union of the primitive types string, number and boolean
  • index type query operator: Given a type T, the type keyof T is the type of a key of T
  • indexed access operator: Given a type T and K extends keyof T, the type T[K] is the type of a value associated with a key of type K
  • intersection types: Given a type T, the type keyof T & string restricts a key to be a string
  • and much more

Example: restrict object access

In the following we restrict the set of valid keys when accessing object values using the indexed access operator:

function select<T, K extends keyof T & string, V extends T[K]>
             (obj: T, key: K): V {
    return obj[key] as V;
}

const config = {
    host: "localhost",
    port: 80,
    0: {
        self: () => config
    }
};

// inferred as string
const host = select(config, "host");

// inferred as number
const port = select(config, "port");

// 💥 Argument of type '0' is not assignable
//    to parameter of type '"host" | "port"'.
const never = select(config, 0);

// 💥 Argument of type '"protocol"' is not assignable
//    to parameter of type '"host" | "port"'.
const ever = select(config, "protocol");

Pimping React Router using the indexed access operator

In the following I want to show you a more complex application of the indexed access operator.

Short introduction to React

Currently I write web applications in TypeScript using Faccebook's React library. React applications are composed in a functional way, using React components.

interface IPersonProps {
    name: string;
}

class Person extends React.Component<IPersonProps> {
    public render() {
        return (
            <div>{this.props.name}</div>
        );
    }
}

Instead of instantiating the class using the new keyword, the JSX library provides us with HTML-like syntactic sugar:

const person = <Person name="Daniel" />;

A React component may have children — such trees are translated to HTML DOM elements.

Short introduction to React Router

React Router is a library that allows us to conditionally render a page, depending on the browser's current url. Routes are very much like switch cases. The Switch component searches the first Route path that matches the browser's current location (following certain rules) and renders the Route's component.

import React from 'react';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';

class App extends React.Component<{}> {
  public render() {
    return (
      <BrowserRouter>
        <Switch>
          <Route path='/login' component={Login} />
          <Route exact path='/' component={Home} />
          <Redirect to='/' />
        </Switch>
      </BrowserRouter>
    );
  }
}

Within the render() method, we typically mix custom elements with standard HTML elements. Dynamic pages are created by mutating a component's internal state, which in turn triggers re-rendering of parts of the DOM tree. But that's a different story.

Taking a look at the example, we see two urls: /login and /. These are strings. The compiler will not complain, when we change a route, say /login -> /signin, whithout fixing all links within the application. Instead we will have a problem at runtime.

<Link to='/login'>click me</Link>

From the maintainability perspective, it starts to get even worse when having routes containing parameters:

<Route path='/user/:id'
       component={
           (props: {id: string}) => <User id={props.id} />
       }
/>

<Link to="/user/daniel">click me</Link>

If the user id type changes from string to number, the compiler won't complain about wrong links.

Creating a Type Safe React Router

The indexed access operator (see above), is a powerful tool to restrict our types.

Our goal is to make the compiler complain, if we use malformed routes or links. Also we want to ensure that we use the correct url parameter types.

In order to do so, we first need to invent a notion for route types. When designing a new API, I always start from the user-perspective and try to find the minimum amount of information that is necessary.

interface IRoutes {
    '/user/:id': { id: string },
    '/login': {},
    '/': {}
}

A Route represents a browser location. It consists of path segments and possibly has path variables. The variables are typed. Our interface IRoutes reflects that.

Now we are able to parameterize our magic TypedReactRouter class in order to get substitutes for certain React Router types, namely Link, Redirect and Route.

// before
import { BrowserRouter, Link, Redirect, Route, Switch } from 'react-router-dom';

// after
import { BrowserRouter, Switch } from 'react-router-dom';
import { TypedReactRouter } from 'typed-react-router';

const { Link, Redirect, Route } = new TypedReactRouter<IRoutes>();

Existing router code will still compile when doing so (but parameterized links need to be consolidated, see below).

As a bonus, TypedReactRouter uses React Router as peer dependency. More specifically, the dependency is not bundled with TypedReactRouter. Given that, TypedReactRouter automatically benefits from bug fixes and new features, without the need of being updated.

Now we are able to write type safe routes:

// type safe!
<Route path='/user/:id'
       component={
           (props: {id: string}) => <User id={props.id} />
       }
/>

// 💥 does not compile because id is of type 'number' instead of 'string'
<Route path='/user/:id'
       component={
           (props: {id: number}) => <User id={props.id} />
       }
/>

// 💥 does not compile because path '/user/:uid' is not declared in IRoutes
<Route path='/user/:uid'
       component={
           (props: {uid: string}) => <User id={props.uid} />
       }
/>

// before
<Link to="/user/daniel">click me</Link>

// after
<Link to="/user/:id" params={{id: "daniel"}}>click me</Link>

The application will not compile anymore if

  • we use a path that is not declared in IRoutes
  • a variable name is wrong
  • the type of a variable is wrong
  • a route is changed without fixing the links
  • ...

Additionally, we get code completion for free in our favorite IDE.

Please see it in action! The video also shows the TypedReactRouter implementation — this blog post can't hold that many generics without having horizontal scrollbars ;)

(Oh my gosh, I need to speak faster — it was a long day and I'm a bit sleepy. I suggest to play it at least at 1,5x speed 😅 )

I am thinking about releasing typed-react-router on https://npmjs.org after polishing it a bit. Furthermore I made also React Intl type-safe, which is used for internationalization / formatted messages.


We took a look at the type system of TypeScript which feels to me much more sophisticated than Java's type system.

Stay safe!

- Daniel