Recently, I have been programming in both Rust and TypeScript at the same time, which has made me very aware of which language features I miss the most when moving from one to the other.

One such thing that I wish TypeScript or JavaScript had copied from Rust is how expression are handled. To explain this, let me explore the example of assigning a value to a variable depending on some if-statement. Say you have some code in where you want to assign the value of background to 'white' if it is daytime and 'black' if it is nighttime. In TypeScript or JavaScript, this might look something like this:

let background;
if (daytime) {
    background = 'white';
} else {
    background = 'black';
}

This code runs perfectly fine, but in a project where a lot more is going on, we might want to improve a few things:

  • If we later come back and remove one of the branches, the background color may be left undefined.
  • The background variable should be const to avoid mutability and keep it from being changed inadvertently in other places.
  • We should avoid repeating the variable name to improve readability and reduce the risk of assigning to the wrong thing.

The first point on never leaving background undefined is not as important when using TypeScript instead of JavaScript, because TypeScript will complain if you use the variable without checking that it is defined first. To improve on the two last points, however, we can use a common one-liner that JavaScript and TypeScript inherited by being C-like languages:

const background = daytime ? 'white' : 'black';

Great!

I often rewrite expressions like this in TypeScript. The readability is a bit reduced in more complex cases, but it is okay for simple expressions like the one above.

Setting the background color based on something a bit more complex

Let us now move on to a more complex example, where the background color is calculated differently in the two cases. We will assume that there is a function rgb that takes three numbers between 0.0 and 1.0 and returns a properly encoded color value for whatever purpose this application needs. In the daytime case, we will let the red value vary with the hour of day and at nighttime, we will let the blue value vary with the phase of the moon (for whichever reason you like).

const green = 0.8;
let background;
if (daytime) {
    const hour = getHour();
    const red = hour / 24.0;
    background = rgb(red, green, 1.0);
} else {
    const moonPhase = calculateMoonPhase();
    const blue = moonPhase / 8.0;
    background = rgb(1.0, green, blue);
}

In this case, we will have a hard time expressing it with a one-liner like above, but there are two ways of making the background variable const again.

We could turn the two branches of the if-else-statement into functions:

function daytimeColor(green: number) {
    const hour = getHour();
    const red = hour / 24.0;
    return rgb(red, green, 1.0);
}

function nighttimeColor(green: number) {
    const moonPhase = calculateMoonPhase();
    const blue = moonPhase / 8.0;
    return rgb(1.0, green, blue);
}

const background = daytime ? daytimeColor() : nighttimeColor();

Or we could use an immediately-invoked function expression (IIFE):

const background = (() => {
    if (daytime) {
        const hour = getHour();
        const red = hour / 24.0;
        return rgb(red, 1.0, 1.0);
    } else {
        const moonPhase = calculateMoonPhase();
        const blue = moonPhase / 8.0;
        return rgb(1.0, 1.0, blue);
    }
})();

I frequently use both these “tricks” depending on the context, but I am not too entirely happy with them.

The first option, where I create two new functions, has a couple of downsides. It requires anyone reading the code to jump back and forth to find and read the implementation. This is a good thing if the functions are big and contain a lot of logic, but is unnecessary overhead when there is little code, like above. It becomes necessary to come up with a name for something that is just an intermediate calculation. In addition, any values in the current scope needs to be passed into the function, such as the green value in the example above.

The second option, using IIFE, has the drawback of, well, introducing an IIFE. While IIFEs are quite common in the JavaScript world, I have met plenty of developers who are not familiar with them at all. They also require a fair amount of nonsense boilerplate syntax.

It took some time to understand what all those parentheses meant the first time I encountered an IIFE myself:

(() => { ... })()

Furthermore, an IIFE in JavaScript could mean additional allocations, because the function needs to be created before it is run. In hot code paths, this could lead to performance issues.

Rust lets you use if-expressions when assigning values

In Rust, we can achieve what we want in a much more elegant way. The result of an if-expression can be used to assign a value directly:

let background = if daytime {
    let hour = get_hour();
    let red = hour / 24.0;
    rgb(red, 1.0, 1.0)
} else {
    let moon_phase = calculate_moon_phase();
    let blue = moon_phase / 8.0;
    rgb(1.0, 1.0, blue)
};

(Note that let in Rust is similar to const in JavaScript - a let in JavaScript is more like a let mut in Rust).

I have to admit that code snippets like this looked quite unfamiliar to me at first. Not only was assigning something to an if-statement complete new to me (apart from the C-like one-line discussed above), but the two lines that call the rgb function also do not end with a semicolon. To me, that looked more like a mistake that proper code, but it turns out to be a really powerful feature in Rust: Any expression, be it just a block, an if-statement or anything similar, will return the value of the last expression not ended with a semicolon.

This turns out to be useful also in the case where you do not have an if-statement, but just want to keep some variables out of the outer scope:

let background = {
    // red is only defined in this inner scope
    let red = 0.5; 
    rgb(red, 1.0, 1.0)
};

let foreground = {
    // blue is only defined in this inner scope
    let blue = 0.8;
    rgb(1.0, 1.0, blue)
};

It also works the same way for functions, which means that you do not have to write return explicitly, like in TypeScript:

fn my_color(): Color {
    let red = 1.0;
    rgb(red, 0.1, 0.2)
}

The first time I saw a function in Rust that did not have a return on the last line, I was a bit upset, because I thought it would be hard to determine if the function returned void, or () in Rust-speak, or an actual value. The only way to know is to look for the semicolon.

However, when I understood that this is part of a general rule that any expression can return and be used in assignments and many other places, it made me realize that this actually simplifies a lot of code.