Rust types for JavaScript Developers

Types in JavaScript

Let’s start with something you know.

Here is a JavaScript function.

function addTwo(my_num) {
return my_num + 2;
}

The addTwo function takes an argument and adds a number of value 2 to it.

You already know this, but what I just said is the key insight into what types are.

A number of value 2

The value 2 is a number type.

That is, types and values are different.

A type (like number) is a bag of values (like 2,3,4,5) that are valid for that type.

A number in JavaScript is a type that contains all values from -9007199254740991 to 9007199254740991.

The value 2 is a number type

Similarly, the value false is a boolean type


Types in Rust

Let’s take that knowledge and look at the same function written in Rust.

fn add_two(my_num: f64) -> f64 {
return my_num + 2.0
}

There are a lot of similarities here.

fn instead of function.

snake_case instead of camelCase.

The biggest difference is that we have to specify the types we’re working with for the function argument and the return type.

The type we’ve specified is an f64, which is the same number implementation that we use in JavaScript.

If you want to get specific, an f64 type can be explained in two parts. The f stands for “float”, which means we get to use decimal places, and the 64 defines what the largest number we can use is.

With just this knowledge, we can understand what the f32 type means. If we want to use a function that accepts an f32 the maximum value we can pass it is smaller than if we used an f64.

Why does Rust have so many number types?

Rust has many more number types than JavaScript... but why?

Let’s revisit the function we wrote earlier: addTwo.

If we pass in the value 4, we get the value 6. This is what we wrote the function to do and is what we expected.

If we pass in the value 4.0, we still get the value 6. So far so good.

If we pass in the value "something", which is a string... JavaScript will happily concatenate 4 to the end of the value. Our function now both accepts and returns a string, which is different type than when we passed in a number!

The same thing happens if we pass in an array of strings. The whole array gets coerced to a string itself, with 4 concatenated onto the end of the string.

addTwo(4);
// 6
addTwo(4.0);
// 6
addTwo("something");
// 'something4'
addTwo(["a", "b", "c"]);
// 'a,b,c4'

In JavaScript this may or may not be what we expected, depending on how much you’ve used JavaScript but in Rust, this kind of “pass any value in and we’ll figure it out” behavior doesn’t work.

Consider the following full Rust program that uses our original add_two function.

fn main() {
let output = add_two(4.0);
dbg!(output);
}
fn add_two(a: f64) -> f64 {
a + 2.0
}

If we pass in 4.0, everything is great.

If we change add_two like we did in the JavaScript example, we start running into problems when we go to compile our program.

fn main() {
let output = add_two(4);
dbg!(output);
}
fn add_two(a: f64) -> f64 {
a + 2.0
}

A 2 in Rust suddenly isn’t an f64. We have to include a decimal place for it to be considered an f64.

Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
--> src/main.rs:2:24
|
2 | let output = add_two(4);
| ^
| |
| expected `f64`, found integer
| help: use a float literal: `4.0`

The same happens for the other examples. Consider the error messages for add_two("something"), and add_two(vec!['a','b','c']).

error[E0308]: mismatched types
--> src/main.rs:2:24
|
2 | let output = add_two("something");
| ^^^^^^^^^^^ expected `f64`, found `&str`
error[E0308]: mismatched types
--> src/main.rs:2:24
|
2 | let output = add_two(vec!['a','b','c']);
| ^^^^^^^^^^^^^^^^^ expected `f64`, found struct `Vec`
|
= note: expected type `f64`
found struct `Vec<char>`

They’re all the same error message: mismatched types.

A Vec<char> is not an f64.

A string is also not an f64.

In JavaScript, the types are assigned and checked when our program is running. “Can + be used on the value in my_num" is a runtime decision.

In contrast to JavaScript, Rust checks the types we told it we want to use at compile time. So when we say we want to operate on values with a type of f64, and we try to pass in a value whose type is not an f64, Rust warns us before finishing the compilation.


If we continue thinking about types as bags of potential values, we can understand the differences between the possible implementations.

Our first option involves changing the number type. The value 4 when written without a decimal, is an integer. So we can change the function type to operate on unsigned integers: u64. These are numbers without decimal places. Note that if we want to do this, we also have to change 2.0 to 2.

fn main() {
let output = add_two(4);
dbg!(output);
}
fn add_two(a: u64) -> u64 {
a + 2
}

If we move on to the string example, we’re now faced with a decision. What do we actually want to do if this is a string? Adding a string and an integer doesn’t work.

fn main() {
let output = add_two("something");
dbg!(output);
}
fn add_two(a: &str) -> &str {
a + 2
}

and Rust tells us so when we compile

error[E0369]: cannot add `{integer}` to `&str`
--> src/main.rs:7:7
|
7 | a + 2
| - ^ - {integer}
| |
| &str

In this case, I don’t want to add to a string, so I won’t write this version of the function. We would have the same decision to make with the Vec example. What does it mean to “add_two” to a Vec?


Using Rust’s type system, we can guarantee that the values our functions accept and return are the types of values we expect. This gives us more confidence in what our program is doing, and prevents us from misusing functions on types that we never meant to allow in the first place.