Prerequisites

The common requirement for both the Rust and TypeScript projects is the GIT version control system.

Rust Installation

Rust installation involves installing global binaries, e.g., rustc and cargo. This article was written using version 1.38.0.

TypeScript Installation

TypeScript depends on the JavaScript runtime Node.js; must first be installed. This installation involves installing global binaries, e.g., node and npm. This article was written using version 12.13.0.

TypeScript itself is installed as a Node.js project dependency as we will see below.

Rust Project Creation

Project setup involves running the cargo command to scaffold a Rust project:

note: The completed project is available for download.

cargo new r00_hello_world

The project is also initialized as a GIT repository with a .gitignore file. It also includes the sample source file: src/main.rs:

main.rs

fn main() {
    println!("Hello, world!");
}

TypeScript Project Creation

Project setup first requires scaffolding a JavaScript Node.js project by creating a folder and using the npm command:

note: The completed project is available for download.

mkdir t00_hello_world
cd t00_hello_world
npm init --yes

We initialize the project as a GIT repository with:

git init

and create a .gitignore file:
.gitignore

node_modules

We then convert the JavaScript Node.js project into TypeScript by first installing the TypeScript development dependency:

npm install -D typescript

We then create a TypeScript configuration file: tsconfig.json. Here we use the microsoft/TypeScript-Node-Starter example.

We update the .gitignore file to ignore the output directory: dist:

.gitignore

node_modules
dist

We update the package.json; updating the main value and adding a start and build to scripts:

package.json

{
  "name": "t00_hello_world",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^3.6.4"
  }
}

Finally, we create the source file: src/index.ts:

index.ts

console.log('Hello World!');

Rust Build

During development, we use the following command to build a Rust project:

cargo build

The output is a compiled OS executable: target/debug/r00_hello_world.

For release, an optimized build, we use the following command:

cargo build --release

The output is a compiled OS executable: target/release/r00_hello_world.

TypeScript Build

We use the following command to build a TypeScript project:

npm run build

The output is a transpiled JavaScript file: dist/index.js.

Rust Execute

We simply execute the compiled OS executable as we would any other:

./target/debug/r00_hello_world

We can also use the command:

cargo run

to both build and run.

TypeScript Execute

We execute the transpiled JavaScript file using the npm script:

npm start

or we can execute it directly:

node dist/index.js

In either case, we are executing our project on top of the JavaScript runtime.

Variables and Mutability

Let us walk through the examples in the Rust Book Variables and Mutability section and contrast them with TypeScript.

With Rust, a variable set with let as shown is immutable.

main.rs

...
fn main() {
    // IMMUTABLE
    let x = 5;
    println!("The value of x is: {}", x); // 5
    // x = 6; // CANNOT ASSIGN TWICE TO IMMUTABLE VARIABLE 
    // println!("The value of x is: {}", x);
    ...
}

In TypeScript, the equivalent is a variable set with const.

index.ts

...
// IMMUTABLE
const x = 5;
console.log(`The value of x is: ${x}`); // 5
// x = 6; // CANNOT ASSIGN BECAUSE CONSTANT
// console.log(`The value of x is: ${x}`)
...

note: In both Rust and TypeScript the variable types are inferred from the right-hand-side; thus explicit typing is not required.

With Rust, a variable set with let mut as shown is mutable.

main.rs

...
fn main() {
    ...
    // MUTABLE
    let mut y = 5;
    println!("The value of y is: {}", y); // 5
    y = 6;
    println!("The value of y is: {}", y); // 6
    ...
}

In TypeScript, the equivalent is a variable set with let.

index.ts

...
// MUTABLE
let y = 5;
console.log(`The value of y is: ${y}`); // 5
y = 6;
console.log(`The value of y is: ${y}`); // 6
...

With Rust, a variable set with let and let mut can be set at run-time:

main.rs

fn another_function(x: i32) -> i32 {
    return x + 1;
}

fn main() {
    ...
    // RUN-TIME ASSIGNMENT
    let z = another_function(5);
    println!("The value of z is: {}", z); // 6
    let mut zz = another_function(5);
    zz = zz + 1;
    println!("The value of zz is: {}", zz); // 7
    ...
}

Likewise for TypeScript for variables set with both const and let:
index.ts

const anotherFunction = (x: number): number => x + 1;
...
// RUN-TIME ASSIGNMENT
const z = anotherFunction(5);
console.log(`The value of z is: ${z}`); // 6
let zz = anotherFunction(5);
zz = zz + 1;
console.log(`The value of zz is: ${zz}`); // 7
...

Rust has another immutable variable type; const. Unlike, let however, const cannot be set at run-time (only set at compile-time). The upper-case const variable names is by convention.
main.rs

n another_function(x: i32) -> i32 {
    return x + 1;
}

fn main() {
    ...
    // CONSTANT
    const MAX_POINTS: i32 = 100000;
    println!("The value of MAX_POINTS is: {}", MAX_POINTS); // 100000
    const ANOTHER_CONSTANT: i32 = 100000 + 1; // COMPILE-TIME ASSIGNMENT
    println!("The value of ANOTHER_CONSTANT is: {}", ANOTHER_CONSTANT); // 100001 
    // const YET_ANOTHER_CONSTANT: i32 = another_function(1); // CANNOT RUN-TIME ASSIGNMENT
    // println!("The value of YET_ANOTHER_CONSTANT is: {}", YET_ANOTHER_CONSTANT);
    ...
}

TypeScript does not have an equivalent variable type. For this situation, one typically uses the TypeScript const variable type and simply name it with upper-case by convention.

Rust allows one to redefine, or shadow, a variable of type let:

main.rs

...
fn main() {
    ...
    // SHADOWING
    let a = 5;
    let a = a + 1;
    let a = a * 2;
    println!("The value of a is: {}", a); // 12
}

note: It is important to observe that the resultant variable a can still be immutable. Also, while not illustrated in this example, the variable types can also differ through shadowing.

TypeScript does not have an equivalent feature, i.e., TypeScript cannot redeclare a block-scoped variable. In TypeScript, one often simply creates a new const variable name in this situation.

Data Types

Now we walk through the examples in the Rust Book Data Typessection; starting first with the Rust scalar types.

main.rs

...
fn main() {
    ...
    // NUMBERS
    let i = 1; // i32
    println!("The value of i is: {}", i); // 1
    let j = 1.1; // f64
    println!("The value of j is: {}", j); // 1.1

    // BOOLEAN
    let b = true; // bool
    println!("The value of b is: {}", b); // true

    // CHARACTER
    let c = 'a'; // char
    println!("The value of c is: {}", c); // a
    ...
}

Let us consider the equivalents (called primitives) in TypeScript:
index.ts

...
// NUMBERS
const i = 1; // number
console.log(`The value of i is: ${i}`); // 1
const j = 1.1 // number
console.log(`The value of j is: ${j}`); // 1.1

// BOOLEAN
const b = true; // boolean
console.log(`The value of b is: ${b}`); // true

// CHARACTER
const c = 'a'; // string
console.log(`The value of c is: ${c}`); // a
...

One big difference is that in JavaScript (and thus TypeScript) all numbers are 64-bit floating point where-as Rust there are a number of integer and floating point types.

Another difference is that JavaScript (and thus TypeScript) has a primitive string type that can contain an arbitrary number of characters; not just a single character.

Rust has two compound types; tuple and array. A tuple being a fixed length ordered list of values of varying types. An array being a fixed length ordered list of values of the same type.

main.rs

...
fn main() {
    ...
    // TUPLE
    let tup = (0, 'a', 1.1); // (i32, char, f64)
    println!("The second value of tup is: {}", tup.1); // a
    let (t1, t2, t3) = tup;
    println!("The value of t1 is: {}", t1); // 0
    println!("The value of t2 is: {}", t2); // a
    println!("The value of t3 is: {}", t3); // 1.1

    // ARRAY
    let arr = [0, 1, 2]; // [i32, 3]
    println!("The second value of arr is: {}", arr[1]); // 1
    let [a1, a2, a3] = arr;
    println!("The value of a1 is: {}", a1); // 0
    println!("The value of a2 is: {}", a2); // 0
    println!("The value of a3 is: {}", a3); // 0
    ...
}

In TypeScript, a tuple is essentially the same as in Rust. An array, however is of arbitrary length, e.g., we can append a value.

index.ts

...
// TUPLE
const tup: [number, string, number] = [0, 'a', 1.1]; // [number, string, number]
console.log(`The second value of tup is: ${tup[1]}`); // a
const [t1, t2, t3] = tup;
console.log(`The value of t1 is: ${t1}`); // 0 
console.log(`The value of t2 is: ${t2}`); // a
console.log(`The value of t3 is: ${t3}`); // 1.1

// ARRAY
const arr = [0, 1, 2]; // number[]
console.log(`The second value of arr is: ${arr[1]}`); // 1
const [a1, a2, a3] = arr;
console.log(`The value of a1 is: ${a1}`); // 0 
console.log(`The value of a2 is: ${a2}`); // 1
console.log(`The value of a3 is: ${a3}`); // 2
arr[3] = 3;
console.log(`The fourth value of arr is: ${arr[3]}`); // 3
...

Sidebar into Object or Reference

So far, Rust and TypeScript have been relatively similar; however there is a big difference lurking in compound types. The difference is in how each of them store compound types in variables.

In Rust, the value of a compound type variable is the object itself.

main.rs

...
fn main() {
    ...
    // OBJECT OR REFEREENCE
    let mut tup2 = (0, 'a', 1.1); // (i32, char, f64)
    let tup3 = tup2;
    tup2.0 = 1;
    println!("The first value of tup2 is: {}", tup2.0); // 1
    println!("The first value of tup3 is: {}", tup3.0); // 0

    let mut arr2 = [0, 1, 2]; // [i32, 3]
    let arr3 = arr2;
    arr2[0] = 1;
    println!("The first value of arr2 is: {}", arr2[0]); // 1
    println!("The first value of arr3 is: {}", arr3[0]); // 0
}

Observations:

  • We have to use let mut for tup2 in order to mutate the value later
  • Assigning tup2 to tup3 (copying their value) creates a completely new tuple
  • The result is that mutating tup2 has no relevance to tup3
  • Some logic applies to arrays

On the other hand, in TypeScript, the value of a compound type is a reference to the object.

note: In TypeScript, primitive types (boolean, number, string) behave like Rust with the value of the variable being the object itself.

index.ts

...
// OBJECT OR REFERENCE
const tup2: [number, string, number] = [0, 'a', 1.1]; // [number, string, number]
const tup3 = tup2;
tup2[0] = 1;
console.log(`The first value of tup2 is ${tup2[0]}`); // 1
console.log(`The first value of tup3 is ${tup3[0]}`); // 1

const arr2 = [0, 1, 2]; // number[]
const arr3 = arr2;
arr2[0] = 1;
console.log(`The first value of arr2 is ${arr2[0]}`); // 1
console.log(`The first value of arr3 is ${arr3[0]}`); // 1
...

Observations:

  • We can use const for tup2 because we are mutating the object and not the reference
  • Assigning tup2 to tup3 (copying their value) copies the reference to the same tuple object
  • The result that mutating tup2 is really mutating the common tuple object referenced by both tup2 and tup3
  • Same logic applies to arrays

I can already see that this is going to take me some getting used to as I have the TypeScript pattern etched into my brain.

Addendum 11/8/19: Having written a couple of more articles, I have come to realize (believe it was buried in the Rust book too), that the compound types tuple and array are likely only to be used as constants, e.g., days of the week, and as such this difference between Rust and TypeScript is irrelevant. Specifically, we will likely be using the Rust Vec type as the equivalent to TypeScript arrays.

Control Flow

The core concepts around flow of control are virtually identical between Rust and TypeScript; confirming this by walking through examples in the Rust Book Control Flowsection.

fn another_function() {
    println!("Another function.");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

fn abs(x: i32) -> i32 {
    if x >= 0 {
        return x;
    }
    x * -1
}

fn main() {
    // FUNCTIONS
    another_function();
    let x = 0;
    let y = plus_one(x);
    println!("The value of y is: {}", y); // 1
    let a = -1;
    let b = abs(a);
    println!("The value of b is: {}", b); // 1

    // COMMENTS
    /*
    Rust supports
    mult-line comments.
    */

    // IF EXPRESSIONS
    let number = 3;
    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }

    let number = 6;
    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }

    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };
    println!("The value of number is: {}", number);

    // LOOPS
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {}", result);

    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");

    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

The same implemented in TypeScript:
index.ts

const anotherFunction = () => console.log('Another funciton.');

const plusOne = (x: number): number => x + 1;

const abs = (x: number): number => {
  if (x >= 0) {
    return x;
  }
  return x * -1;
}

// FUNCTIONS
anotherFunction();
const x = 0;
const y = plusOne(x);
console.log(`The value of y is: ${y}`); // 1
const a = -1;
const b = abs(a);
console.log(`The value of b is: ${b}`); // 1

// COMMENTS
/*
TypeScript supports
multi-line comments.
*/

// IF EXPRESSIONS
const number = 3;
if (number < 5) {
  console.log('condition was true');
} else {
  console.log('condition was false');
}

const number2 = 6;
if (number2 % 4 === 0) {
  console.log('number2 is divisible by 4');
} else if (number2 % 3 === 0) {
  console.log('number2 is divisible by 3');
} else if (number2 % 2 === 0) {
  console.log('number2 is divisible by 2');
}

const condition = true;
const number3 = condition ? 5 : 6;
console.log(`The value of number3 is: ${number3}`);

let counter = 0;
let result: number;
while (true) {
  counter += 1;
  if (counter === 10) {
    result = counter * 2;
    break;
  }
}
console.log(`The result is ${result}`);

let number4 = 3;
while (number4 !== 0) {
  console.log(`${number4}!`);
  number4 -= 1;
}
console.log('LIFTOFF!!!');

const arr = [10, 20, 30, 40, 50];
arr.forEach(element => console.log(`the value is: ${element}`));

Observations:

  • The Rust pattern of having the last line of a function being an expression (with no semi-colon) to mean the return value was novel to me
  • Having gotten used to the TypeScript ternary operator, found the Rust equivalent a bit verbose
  • With Rust, having a loop return a value is novel to me

Next Steps

We dive into the complexities of ownership in the next article: Learning Rust by Contrasting with TypeScript: Part 3.

#rust #TypeScript

Learning Rust by Contrasting with TypeScript
3.30 GEEK