Learning Rust with TypeScript

Today I’m starting a new tutorial series about “Learning Rust by Contrasting with TypeScript”. It’s a complete beginner’s guide to start learning Rust with TypeScript.

Tutorials in the Series

  1. Part 1 - Getting Started.
  2. Part 2 - We explore variables, mutability, and data types
  3. Part 3 - Diving into the complexities of ownership
  4. Part 4 - The power of references
  5. Part 5 - Structuring with structs.
  6. Part 6 - Exploring the unexpected power of enums.
  7. Part 7 - Exploring Rust’s module system.
  8. Part 8 - Looking at collections.
  9. Part 9 - Error handling and generic data types.

Learning Rust with TypeScript: Part 1 - Getting Started

Motivation

Watch the video.

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:

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:

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:

node_modules
dist

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

{
  "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:

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.

Seeing Rust as a potential successor to TypeScript, we go through through the Rust Bookwith TypeScript in mind.

Learning Rust with TypeScript: Part 2 - We explore variables, mutability, and data types.

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.

...
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.

...
// 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.

...
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.

...
// 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:

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:

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.

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:

...
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.

...
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:

...
// 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.

...
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.

...
// 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.

...
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.

...
// 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:

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

The code for this article is available to download; Rust download and TypeScript download.

Learning Rust with TypeScript: Part 3 - Diving into the complexities of ownership.

Let us walk through the examples in the Rust Book What is Ownership?section and contrast them with TypeScript.

note: Unlike the previous articles, reading the Rust Book on this topic first is a must (and a pleasure).

Variable Scope

First, we remind ourselves how variable scope works in Rust:

fn do_something() {
    let x = 5; // x comes into scope
    println!("The value of x is: {}", x); // 5
} // x goes out of scope
...
fn main() {
    do_something();
    ...
}

Here we would say that the value of 5 is uniquely owned by the variable x. Once x goes out of scope, Rust immediately invalidates the data (the 5) owned by it; in this case the value is removed from the stack.

note: The scalar (i32, bool, etc) and primitive compound types (tuple and array of scalars) are examples of values that are stored on the stack; apparently this is related to these types have the Copy trait (something that we will explore later).

Let us interpret what is happening with the equivalent TypeScript code:

const doSomething = () => {
  const x = 5;
  console.log(`The value of x is: ${x}`); // 5
}

doSomething();

Looking at the code, might assume that once the doSomething function executes the value of 5 can be immediately reclaimed. But in reality, this is something that is handled by the JavaScript garbage collector and thus outside of our domain of knowledge.

note: Remember, TypeScript is transpiled to JavaScript before it is executed.

Some high-level languages, such as JavaScript, utilize a form of automatic memory management known as garbage collection (GC). The purpose of a garbage collector is to monitor memory allocation and determine when a block of allocated memory is no longer needed and reclaim it. This automatic process is an approximation since the general problem of determining whether or not a specific piece of memory is still needed is undecidable.

— MDN — Memory Management JavaScript

The String Type

We now are introduced to Rust’s String type:

...
fn do_something_else() {
    let s = String::from("hello"); // s comes into scope
    println!("The value of s is: {}", s); // hello
} // s goes out of scope

fn main() {
    ...
    do_something_else();
}

Observations:

  • Since Strings are mutable, their associated data is split into the variable’s value, a pointer stored on the stack and the pointed to data stored on the heap
  • Here I struggled a bit; but I think we would say that both the pointer value and the heap data are uniquely owned by the variable
  • The String type does not have the Copy trait; rather it has the Drop trait (not sure what this exactly means but apparently we will learn more about this later).
  • When s goes out of scope, the String’s drop feature (we will learn more about this later too) is executed. The data (stack pointer value and heap data) are deallocated

With TypeScript, the string type is, interestingly enough, immutable. As such it is not fully comparable to Rust’s String type; we will come back to TypeScript strings in a bit. Rather, we can consider the TypeScript array type:

...
const doSomethingElse = () => {
  const arr = [0, 1, 2];
  console.log(`The first value of arr is: ${arr[0]}`); // 0
}
...
doSomethingElse();

Observations:

  • The array type is mutable
  • In TypeScript we would say the value of arr is a reference to the array object; essentially means the same as the concept of a pointer with the Rust String example
  • Again, we are reliant on the JavaScript garbage collector to reclaim memory

note: In Rust, the term reference has a special meaning. That is why we use the more generic term pointer.

Mutable with Pointer Values

Let us explore how to mutate a Rust String:

...
fn do_even_more() {
    let s = String::from("hello");
    // s.push_str(", world!"); // CANNOT BORROW AS MUTABLE
    println!("The value of s is: {}", s); // hello
    let mut t = String::from("hello");
    t.push_str(", world!"); // CANNOT BORROW AS MUTABLE
    println!("The value of t is: {}", t); // hello, world!
}

fn main() {
    ...
    do_even_more();
    ...
}

Observations:

  • In order to mutate a String, the variable must be a let mut

Mutating a TypeScript array:

...
const doEvenMore = () => {
  const arr = [0, 1, 2];
  arr.push(3);
  console.log(`The fourth value of arr is: ${arr[3]}`); // 3
}
...
doEvenMore();
...

Observations:

  • TypeScript (and JavaScript for that matter) does not have a similar immutable concept for references, i.e., any reference to an object allows one to operate on it as one pleases

note: In TypeScript, const only prevents one from re-assigning the reference value to another object

Copy Versus Move

Let us explore the meaning for assignment for pointers in Rust:

...
fn do_something_crazy() {
    // COPY
    let x = 5;
    println!("The value of x is: {}", x); // 5
    let y = x;
    println!("The value of y is: {}", y); // 5
    println!("The value of x is: {}", x); // 5
    
    // MOVE
    let s = String::from("hello");
    println!("The value of s is: {}", s); // hello
    let t = s;
    println!("The value of t is: {}", t); // hello
    // println!("The value of s is: {}", s); // BORROW OF MOVED VALUE
}

fn main() {
    ...
    do_something_crazy();
    ...
}

Observations:

  • As we saw in an earlier article assigning variables (of types with Copy trait) to another variable copies the value to the new variable
  • On the other hand, assigning variables (of types with Drop trait) to another variable copies the pointer value, invalidates the source variables pointer value, and moves the ownership of heap data to the target variable

note: This idea of moving the heap data’s ownership is my best interpretation of what is meant by move here.

Let us explore the meaning of assignment for references in TypeScript:

...
const doSomethingCrazy = () => {
  const x = 5;
  console.log(`The value of x is: ${x}`); // 5
  const y = x;
  console.log(`The value of y is: ${y}`); // 5

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

...
doSomethingCrazy();
...

Observation:

  • Similar to Rust, assigning a TypeScript primitive variable to another variable copies the value
  • Assigning reference variables to another variable copies the reference value. TypeScript does not enforce any restrictions on the number of references to an object

Ownership and Functions

With Rust, from an ownership perspective, passing a parameter to a function (and returning a value) behaves much like assigning variables.

...
fn makes_copy(some_integer: i32) { 
    println!("The value of some_integer is: {}", some_integer); // 5
} 

fn takes_ownership(some_string: String) { 
    println!("{}", some_string); // hello
} 

fn gives_ownership() -> String { 
    let some_string = String::from("hello");
    some_string
}

fn main() {
    ...    
    // OWNERSHIP AND FUNCTION PARAMETERS
    let x = 5;
    makes_copy(x);
    println!("The value of x is: {}", x); // 5

    let s = String::from("hello");
    takes_ownership(s);
    // println!("The value of s is: {}", s); // BORROW OF MOVED VALUE

    // OWNERSHIP AND FUNCTION RETURNS
    let s1 = gives_ownership();
    println!("The value of s1 is: {}", s1); // hello
}

Likewise for TypeScript; i.e., matches TypeScript variable assignment:

...
const makesCopy = (someNumber: number) => {
  console.log(`The value of someNumber is: ${someNumber}`); // 5
}

const makesReferenceCopy = (someArray: number[]) => {
  console.log(`The first value someArray is: ${someArray[0]}`); // 0
}

const returnsReference = (): number[] => [0, 1, 2];
...
// FUNCTION PARAMETERS
const x = 5;
makesCopy(5);
console.log(`The value of x is: ${x}`); // 5

const arr = [0, 1, 2];
makesReferenceCopy(arr);
console.log(`The first value arr is: ${arr[0]}`); // 0

// FUNCTION RETURNS
const arr1 = returnsReference();
console.log(`The first value arr1 is: ${arr1[0]}`); // 0

The examples from this article are available for download: Rust download and TypeScript download.

Learning Rust with TypeScript: Part 4 - The power of references.

References and Borrowing

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

In the previous article, we observed that when we passed a String (pointer) variable into a function, that variable’s pointer value was invalidated and the ownership of the string (heap) data was moved onto the parameter in the function’s scope.

This means that we can no longer use the invalidated variable in the calling function; this is an unfortunate situation.

The solution is that we can instead pass a reference (a pointer to the pointer) into the function. This does not invalidate the variable nor move the ownership of the string data.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len); // hello 5
}

fn calculate_length(s: &String) -> usize {
    let len = s.len();
    // s.push_str(", world!"); // CANNOT BORROW AS MUTABLE
    len
}

Observations:

  • We can continue to use the s1 variable after passing a reference of it into the calculate_length function
  • Also observe, we cannot use the reference s to mutate the String; the function’s signature guarantees the function cannot mutate the s parameter
  • In this case, we can refer to calculate_length a pure function; no side-effects

TypeScript neither has a concept of object ownership nor of a reference (in the Rust pointer to a pointer sense). Instead, we simply can pass a TypeScript reference into a function without impacting our ability to continue to use that reference in the calling code.

const calculateLength = (arr: number[]): number => {
  const len = arr.length;
  arr.length = 0; // UNEXPECTED SIDE EFFECT
  return len;
}

const arr = [0, 1, 2];
const len = calculateLength(arr);
console.log(`The length of ${arr} is ${len}`); // [] 3

Observations:

  • Notice that the arr variable continues to be usable after passing it into the calculateLength function
  • Also, because TypeScript does not have controls to limit the mutability of referenced objects, we can create unexpected size effects in functions, e.g., in this case calculateLength empties the array (unexpected to the consumer of the function, that is)

Mutable References

In the previous Rust example we could not mutate (purposefully) the String in the calculate_length function. Let us say, however, we wanted to be able to mutate the String? You guessed it, Rust supports mutable references.

fn main() {
    ...
    let mut s = String::from("hello");
    change(&mut s);
    println!("The value of s is: {}.", s); // hello, world
}
...
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Observation:

  • Not only do we pass in a mutable reference to the change function, the function’s signature actually requires it

Rust has rules limiting the use of mutable references when other references (mutable or not) are in scope. The general idea is that Rust is preventing one from accidentally mutating a referenced value unexpectedly.

fn main() {
    ...
    let mut s1 = String::from("hello");
    let r1 = &s1;
    let r2 = &s1;
    // let r3 = &mut s1; // CANNOT BORROW MUTABLE SINCE ALREADY BORROWED
    // change(r3);
    // println!("{}, {}, and {}", r1, r2, r3);
    println!("{} and {}", r1, r2); // hello hello
}
...
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Observations:

  • While we can create an unlimited number of immutable references, e.g., r1 and r2, we cannot create the mutable r3 reference while other references are in scope
  • Unlike variable scope, the scope of references is between when they are created and last used (a little weird).

In TypeScript, we have no such protections around references; any variable reference essentially allows one to mutate the object and one can create an unlimited number of them.

...
const change = (arr: number[]) => {
  arr.push(100);
}
...
const arr1 = [0, 1, 2];
const r1 = arr1;
const r2 = arr1;
const r3 = arr1;
change(r3);
console.log(`The value of r1 is: ${r1}`); // [0, 1, 2, 100]
console.log(`The value of r2 is: ${r2}`); // [0, 1, 2, 100]
console.log(`The value of r3 is: ${r3}`); // [0, 1, 2, 100]

Dangling References

Rust also includes checks to ensure one does not return dangling references, e.g., a reference to something that no longer exist.

...
/*
fn dangle() -> &String { // RETURNS DANGLING REFERENCE
    let s = String::from("hello");
    &s
}
*/

Tried to think of a TypeScript example of a dangling reference, but then was reminded that the garbage collector only reclaims objects that are not in use, i.e., without reachable references to them.

Slices

Let us walk through the examples in the Rust Book The Slice Typesection and contrast them with TypeScript.

First we learn that we can create a new type of immutable reference to a String: a String slice (indicted as a &str type). Pretty straightforward stuff.

The more interesting feature is that because slices are references, rules around mutations and references apply to protect one from making mistakes:

fn main() {
    ...
    let mut phrase = String::from("hello world!");
    let first = first_word(&phrase);
    println!("The value of first is {}.", first); // hello
    // phrase.clear(); // CANNOT BORROW AS MUTABLE
    println!("The value of first is {}.", first); // hello
}
...
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Observations:

  • Here, as long as first is in scope (through the last line of main), we cannot mutate the phrase.
  • Earlier we indicated that we could not create mutable references once another reference in scope. Along the same line, we also cannot mutate the referenced object once another reference is in scope

In TypeScript we do not have slices and thus have to take a different approach:

...
const firstTwo = (arr: number[]): number[] => arr.slice(0, 2);
...
const arr2 = [0, 1, 2];
const first = firstTwo(arr2);
console.log(`The value of first is: ${first}`); // [0, 1]
arr2.length = 0;
console.log(`The value of first is: ${first}`); // [0, 1]
...

Observations:

  • Here we use the slice method to create a completely new array
  • Creating a completely new array, especially if the array is large, is an expensive operation

String Literals

Now that we have introduced String slices, we can now understand that Rust does support string literals. They are just string slices pointing at strings in the code.

fn main() {
    ...
    let hello = "hello";
    println!("The value of hello is {}.", hello); // hello
}
...

This actually matches up against the immutable string type in TypeScript:

...
const hello = 'hello';
console.log(`The value of hello is: ${hello}`); // hello

The examples from this article are available for download: Rust download and TypeScript download.

Learning Rust with TypeScript: Part 5 - Structuring with structs.

Defining and Instantiating Structs

We first illustrate defining a struct (User), creating an instance of User, and creating a mutable variable pointing to it (user); pretty straightforward.

fn main() {
    let mut user = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    user.email = String::from("someone_else@example.com");
    println!("The value of user.username is: {}.", user.username); // someusername123
}

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

One might immediately think the corresponding concept with TypeScript is a class. If, however, one things more carefully a minimal implementation simply consists of an Object.

const user = {
  email: 'someone@example.com',
  username: 'someusername123',
  active: true,
  signInCount: 1,
};

user.email = 'someone_else@example.com';
console.log(`The value of user.email is: ${user.username}`); // someusername123

Factory

Rust provides a very functional approach to standardizing the creation of a struct; a factory function.

fn main() {
    ...
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123")
    );
    println!("The value of user1.username is: {}.", user1.username); // someusername123
}

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Guess what, this is essentially identical to a TypeScript approach:

interface User {
  email: string;
  username: string;
  active: boolean;
  signInCount: number;
}

const buildUser = (email: string, username: string): User => ({
  email,
  username,
  active: true,
  signInCount: 1,
});
...
const user1 = buildUser('someone@example.com', 'someusername123');
console.log(`The value of user.email is: ${user1.username}`); // someusername123

Struct Update Syntax

Rust provides a syntax to create structs based on other structs.

fn main() {
    ...
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123")
    );
    println!("The value of user1.username is: {}.", user1.username); // someusername123

    let user2 = User {
        email: String::from("another@example.com"),
        username: String::from("anotherusername567"),
        ..user1
    };
    println!("The value of user2.username is: {}.", user2.username); // someusername123
}

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

With TypeScript, we can do the same with the Object spread syntax:

interface User {
  email: string;
  username: string;
  active: boolean;
  signInCount: number;
}

const buildUser = (email: string, username: string): User => ({
  email,
  username,
  active: true,
  signInCount: 1,
});

const user: User = {
  email: 'someone@example.com',
  username: 'someusername123',
  active: true,
  signInCount: 1,
};
...
const user1 = buildUser('someone@example.com', 'someusername123');
console.log(`The value of user1.email is: ${user1.username}`); // someusername123

const user2: User = {...user1, email: 'another@example.com', username: 'anotherusername567'};
console.log(`The value of user2.email is: ${user2.username}`); // anotherusername567

Tuple Structs

With Rust, we can type our tuples:

fn main() {
    ...
    let color = Color(0, 0, 0);
    println!("The value of the first color is: {}.", color.0); // 0
}
...
struct Color(i32, i32, i32);

More or less the same thing with TypeScript:

type Color = [number, number, number];
...
const color: Color = [0, 0, 0];
console.log(`The value of the first color is: ${color[0]}`);

Method Syntax

Rust methods are fairly straightforward too.

fn main() {
    ...
    let rect = Rectangle {
        width: 10,
        height: 10
    };
    let area = rect.area();
    println!("The value of area is: {}.", area); // 100 
    let rect1 = Rectangle {
        width: 7,
        height: 7
    };
    let rect1_fits = rect.can_hold(&rect1);
    println!("The value of rect1Fits is: {}.", rect1_fits); // true
}
...
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

With TypeScript here, we switch to using classes to implement methods.

...
class Rectangle {
  width: number;

  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }

  canHold(other: Rectangle): boolean {
    return this .width > other.width && this.height > other.height;
  }
}
...
const rect = new Rectangle(10, 10);
const area = rect.area();
console.log(`The value of area is: ${area}`); // 100
const rect1 = new Rectangle(7, 7);
const rect1Fits = rect.canHold(rect1);
console.log(`The value of rect1Fits is: ${rect1Fits}`); // true

note: Interestingly enough, however, by switching to classes we loose the ability to use the Object spread syntax (used with object literals).

Associated Functions

With Rust we implement associated functions.

fn main() {
    ...
    let square = Rectangle::square(8);
    let square_area = square.area();
    println!("The value of squareArea is: {}.", square_area); //  64
}
...
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Guess what, this is just static methods in TypeScript:

...
class Rectangle {
  static square(size: number): Rectangle {
    return new Rectangle(size, size);
  }
  width: number;

  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }

  canHold(other: Rectangle): boolean {
    return this .width > other.width && this.height > other.height;
  }
}
...
const square = Rectangle.square(8);
const squareArea = square.area();
console.log(`The value of squareArea is: ${squareArea}`); // 64

The examples from this article are available for download: Rust download and TypeScript download.

Let us walk through the examples in the Rust Book Using Structs to Structure Related Datasection and contrast them with TypeScript.

Learning Rust with TypeScript: Part 6 - Exploring the unexpected power of enums.

Enums

Strangely enough, I found enums a bit more challenging to understand than the previous material; found the idea of associating data with the enum variant hard to get my head around.

It wasn’t until the documentation described that the following enum…

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

…could be described as a list of struct-like structures.

Defining an enum with variants such as the ones in Listing 6–2 is similar to defining different kinds of struct definitions, except the enum doesn’t use the struct keyword and all the variants are grouped together under the Message type. The following structs could hold the same data that the preceding enum variants hold:

— Rust Team — Defining an Enum

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

With this in mind, we can then can make sense of enums and explore all their features:

fn main() {
    let coin = Coin::Penny;
    let coin2 = coin;
    // println!("The value of coin is: {:?}", coin); // BORROWED AFTER MOVE
    println!("The value of coin2 is: {:?}", coin2); // Penny
    coin2.display();
    let coin3 = Coin::Quarter(2);
    match coin3 {
        Coin::Penny => {
            println!("Lucky penny!");
        },
        Coin::Nickel => {
            println!("Shiny nickel!");
        },
        Coin::Dime => {
            println!("Small dime!");
        },
        Coin::Quarter(value) => {
            println!("Large quarter with value: {}", value); // 2
        },
        _ => ()
    }
    match coin3 {
        Coin::Quarter(value) => {
            println!("Large quarter with value: {}", value); // 2
        },
        _ => ()
    }
    if let Coin::Quarter(value) = coin3 {
        println!("Large quarter with value: {}", value); // 2
    } 
}

#[derive(Debug)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(i32),
    Euro,
}

impl Coin {
    fn display(&self) {
        println!("Displayed is: {:?}", self); // Penny
    }
}

Observations:

  • When I first wrote this example, I was surprised to see that a variable that contained an enum, e.g., coin, did not support the Copy trait. It turns out that assigning coin to coin2 is a move operation. This is completely understandable if one thinks of an enum variant as a struct-like structure
  • The second match coin3 and the if let statements are equivalent. As a matter of fact, this is the only way I could understand what if let is

While TypeScript has enums, they (for the most part) as just a convenient way to name numbers, e.g., 0, 1, 2, etc. We can mostly approximate our previous example by using a combination of an enum and a class.

enum CoinKind {
  Penny,
  Nickel,
  Dime,
  Quarter,
  Euro,
}

class Coin {
  kind: CoinKind;

  value: number;

  constructor(kind: CoinKind, value: number) {
    this.kind = kind;
    this.value = value;
  }

  display() {
    console.log(`Displayed is ${this.kind}`);
  }
}

const coin = new Coin(CoinKind.Penny, null);
const coin2 = coin;
console.log(`The value of coin is ${coin.kind}`); // 0
console.log(`The value of coin2 is ${coin2.kind}`); // 0
coin2.display(); // 0
const coin3 = new Coin(CoinKind.Quarter, 2);
switch (coin3.kind) {
  case CoinKind.Penny:
    console.log('Lucky penny!');
    break;
  case CoinKind.Nickel:
    console.log('Shiny nickel!');
    break;
  case CoinKind.Dime:
    console.log('Small dime!');
    break;
  case CoinKind.Quarter:
    console.log(`Large quarter with value: ${coin3.value}`); //2
    break;
  default:
}
if (coin3.kind === CoinKind.Quarter) {
    console.log(`Large quarter with value: ${coin3.value}`); // 2
}

Observations:

  • This approach requires that each instance have the same associated data structure (in this case the number value). Notice we had to use a null to represent a unused value
  • Unlike Rusts match statement, the switch statement does not ensure we exhaust the enum’s variants

Option Enum

In writing this article, I have come to better appreciate the following observation.

The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

— The Rust Team — [Defining an Enum](http://The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.)

And come to appreciate the simplicity and power of Rust enums. Find it a bit mind-boggling that the introduction of the built-in Option enum solves the null problem.

enum Option<T> {
    Some(T),
    None,
}

Here is a simple example of using the Option enum:

fn main() {
    ...
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}


fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

Here is the TypeScript version of the same idea:

...
const plusOne = (x: number): number => {
  if (x === null) {
    return null;
  }
  return x + 1;
};
const five = 5;
const six = plusOne(five);
const none = plusOne(null);
console.log(six); // 6
console.log(none); // null

const len = (str: string): number => {
  if (str === null) {
    return null;
  }
  return str.length;
}
const abc = 'abc';
const three = len(abc);
const nope = len(null);
console.log(three); // 3
console.log(nope); // null

Observation:

  • First observation is that neither TypeScript or strict linting (added ESLint to this example) enforced the null check in either the plusOne or len functions
  • In the case of the plusOne function, omitting the null check caused a null parameter to be treated as 0 and thus the function would return 1
  • In the case of the len function, omitting the null check caused a run-time error when passed a null parameter

The examples from this article are available for download: Rust download and TypeScript download.

Let us walk through the examples in the Rust Book Enums and Pattern Matchingsection and contrast them with TypeScript.

Learning Rust with TypeScript: Part 1 - Exploring Rust’s module system.

Packages and Crates

Going to skim through the section on Packages and Cratesas we are going to focus more on comparing the Rust and TypeScript module systems.

A Rust package delivers:

  • (0, n) Binary (executable) crates; first is src/main.rs, rest come from files in src/bin. Binary crate is built from (1, n) modules
  • (0, 1) Library crate; is src/lib.rs. Library crate can deliver (1, n) modules
  • As least one binary or library crate

A TypeScript package can deliver:

  • (0, n) Executable (interpreted by node.js) modules; values from bin value in package.json
  • (0, n) Module files; default is main value in package.json; rest from package folder hierarchy

note: Modules files can be either applications or libraries.

In our examples, we have been building a Rust package that consists of a single binary crate consisting of a single module. The TypeScript package consists of a single application module.

Modules

The remaining materials on Managing Growing Projects with Packages, Crates, and Modulesis a bit hard to understand until one also reads the Rust By Example material on Modules.

Our Rust example that is available for download consists of four modules:

+ main (file)
|
+ my_mod (folder)
    |
    + nested (file)
    |
    + private_nested

Observations:

  • The “main module” is delivered by a file src/main.rs; struggled a bit to call this a module as it only serves as the entry point for the binary (executable) crate
  • The my_mod module is delivered by a folder named the same as the module and a specially named file; src/my_mod/mod.rs
  • The nested module is delivered by a file named the same as the module; src/my_mod/nested.rs
  • The private_nested module is delivered in the same file as the my_mod module

One big difference between Rust and TypeScript modules is that Rust modules have names and TypeScript modules do not (they are just files). Related to this, a Rust file can contain multiple modules while TypeScript files cannot (as a module equals a file).

Our comparable TypeScript example that is available for download consists of three modules.

+ index (file)
|
+ myMod (folder)
    |
    + nested (file)

Observations:

  • As with the Rust example, the “index module” is odd in that it only serves as the entry point for the application; src/index.ts
  • One module is delivered by a folder, myMod, and a specially named file; src/myMod/index.ts
  • Another module is delivered by a file; src/myMod/nested.ts

Another big difference between Rust and TypeScript is that Rust modules are structured hierarchically (and when defined in files, the hierarchy is reflected in the file / folder structure) and TypeScript modules have a flat structure, i.e., folder and file naming strategies simply exists to help organize the files.

In the Rust example, we see that the nested module is a hierarchical child of my_mod module.

mod my_mod;
...
fn main() {
    function();
    my_mod::function();
    ...
    my_mod::nested::function()
    ...
}

In the TypeScript example, both myMod and nested are flatly imported.

import {
  callPublicFunctionInMyMod,
  func as myModFunc,
  indirectAccess,
  publicFunctionInCrate,
} from './myMod';
import { func as nestedFunc } from './myMod/nested';
...
func();
myModFunc();
...
nestedFunc();
...

Observations:

  • One side-effect of the flatness of TypeScript modules, is that one must manage naming collisions, e.g,. all three modules have a func member

Because Rust modules are organized into a hierarchy, Rust’s privacy rules involve it.

The way privacy works in Rust is that all items (functions, methods, structs, enums, modules, and constants) are private by default. Items in a parent module can’t use the private items inside child modules, but items in child modules can use the items in their ancestor modules.

But you can expose inner parts of child modules code to outer ancestor modules by using the pub keyword to make an item public.

— Rust Team — Paths for Referring to an Item in the Module Tree

In addition, the pub keyword can take parameters for fine grain control, e.g.:

...
pub fn function() {
    println!("called `my_mod::nested::function()`");
}
...
fn private_function() {
    println!("called `my_mod::nested::private_function()`");
}
...
// Functions declared using `pub(super)` syntax are only visible within
// the parent module
pub(super) fn public_function_in_super_mod() {
    println!("called `my_mod::nested::public_function_in_super_mod()`");
}
...

TypeScript, on the other hand, being flat only supports items being exported (public) or not (private), e.g.,

...
export const func = (): void => {
  console.log("called nested exported 'func()'");
};const privateFunction = (): void => {
  console.log("called nested private 'func()'");
};
...
// NO EQUIVALENT TO pub(super)
export const publicFunctionInSuperMod = (): void => {
  console.log("called nested exported 'publicFunctionInSuperMod()'");
};

Learning Rust with TypeScript: Part 8 - Looking at collections.

Vector (Same Type)

As Rust’s array is of fixed length, we are more likely going to use a Vector. Here we explore Vectors with items of the same type:

fn main() {
    // VECTOR (SAME TYPE)
    let mut v = vec![1, 2, 3, 4];
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
    let one = v.get(0);
    if let Some(i) = one {
        println!("The value of the first item of v is: {}", i); // 1
    }
    for i in &v {
        println!("{}", i); // 1 2 3 4 5 6 7 8
    }
    ...
}
...

Observations:

  • Not fully sure what a macro is (will have to explore later), but the vec! macro is super handy for initializing a Vector
  • We can see the power of the Option enumerable at play here as the get method is not guaranteed to return a value

The TypeScript example is handled similarly with an array; we just need to remember to handle the situation where an indexed value could return undefined.

/* eslint-disable no-console */
// VECTOR (SAME TYPE)
const v = [1, 2, 3, 4];
v.push(5);
v.push(6);
v.push(7);
v.push(8);
const one = v[0];
if (one !== undefined) {
  console.log(`The value of the first item of v is: ${one}`); // 1
}
v.forEach(i => {
  console.log(i); // 1 2 3 4 5 6 7 8
});
...

Vector (Multiple Types)

We can use a Rust enum to handle Vectors with multiple types:

fn main() {
    ...
    // VECTOR (MULTIPLE TYPES)
    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
    let three = row.get(0);
    if let Some(i) = three {
        match i {
            SpreadsheetCell::Int(value) => {
                println!("The value of the first item (i32) of row is: {}", value); // 3
            },
            SpreadsheetCell::Float(value) => {
                println!("The value of the first item (f64) of row is: {}", value);
            },
            SpreadsheetCell::Text(value) => {
                println!("The value of the first item (String) of row is: {}", value);
            },
        }
    }
    ...
}

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

We can accomplish the same with TypeScript with TypeScript types; again we have to remember to handle the undefined case.

...
// VECTOR (MULTIPLE TYPES)
type SpreadsheetCell = number | string;
const row: SpreadsheetCell[] = [3, 'blue', 10.12];
const three = row[0];
if (three !== undefined) {
  switch (typeof three) {
    case 'number':
      console.log(`The value of the first item (number) of row is: ${three}`); // 3
      break;
    case 'string':
      console.log(`The value of the first item (string) of row is: ${three}`);
      break;
    default:
  }
}
...

Observation:

  • In this case we could typeof operator to distinguish between primitive types; in the case we were storing more complex types we could use the instanceof operator

Hash Map

We implement a Rust HashMap:

fn main() {
    ...
    // HASH MAP
    use std::collections::HashMap;
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    let score = scores.get(&String::from("Blue"));
    if let Some(v) = score {
        println!("The value of the Blue item of scores is: {}", v); // 10
    }
    for (key, value) in &scores {
        println!("{}: {}", key, value); // Yellow: 50 Blue: 10
    }

    let teams  = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];
    let scores2: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
    let score2 = scores2.get(&String::from("Blue"));
    if let Some(v) = score2 {
        println!("The value of the Blue item of scores2 is: {}", v); // 10
    }
    for (key, value) in &scores2 {
        println!("{}: {}", key, value); // Yellow: 50 Blue: 10
    }
}

Observations:

  • Found it interesting that Rust provides default HashMap typing based on how it is used
  • As with Vectors, we can see the power of the Option enumerable at play
  • Found the line creating scores2 a bit of a mystery; my first inclination would have been to use the Iterator’s fold method

We implement the same with TypeScript:

...
// HASH MAP
const scores = new Map<string, number>();
scores.set('Blue', 10);
scores.set('Yellow', 50);
const score = scores.get('Blue');
if (score !== undefined) {
  console.log(`The value of the Blue item of score is: ${score}`);
}
scores.forEach((value, key) => {
  console.log(`${key}: ${value}`); // Blue: 10 Yellow: 50
});

const teams = ['Blue', 'Yellow'];
const initialScores = [10, 50];
const scores2 = new Map<string, number>();
teams.reduce((accumulator, currentValue, i) => {
  accumulator.set(currentValue, initialScores[i]);
  return accumulator;
}, scores2);
const score2 = scores2.get('Blue');
if (score2 !== undefined) {
  console.log(`The value of the Blue item of score2 is: ${score2}`);
}
scores2.forEach((value, key) => {
  console.log(`${key}: ${value}`); // Blue: 10 Yellow: 50
});

Observations:

  • While we could have likely used a regular TypeScript object (with some fancy typing), there are reasons to use a Map instead
  • As with arrays, we need to be sure to check for undefined values
  • Here we used the reduce method (much like Rust’s fold)

The examples from this article are available for download: Rust download and TypeScript download.

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

Learning Rust with TypeScript: Part 9 - Error handling and generic data types.

Error Handling

One thing that is special about Rust is that it forces you to handle errors.

Rust’s commitment to reliability extends to error handling. Errors are a fact of life in software, so Rust has a number of features for handling situations in which something goes wrong. In many cases, Rust requires you to acknowledge the possibility of an error and take some action before your code will compile. This requirement makes your program more robust by ensuring that you’ll discover errors and handle them appropriately before you’ve deployed your code to production!

— Rust Team — Error Handling

It is also surprising how Rust’s enum structure is used to solve a wide variety of problems; including error handling with the Result enum.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Here we implement the example, from the Error Handling section, that opens or creates a file:

use std::io;
use std::fs::File;
use std::io::ErrorKind;

const FILE_NAME: &str = "hello.txt";

fn main() {
    let f = open_or_create_file();
}

fn open_or_create_file() -> Result<File, io::Error> {
    let f = File::open(FILE_NAME); 
    match f {
        Ok(file) => Ok(file),
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create(FILE_NAME) {
                Ok(fc) => Ok(fc),
                Err(e) => Err(e)
            },
            _ => Err(error),
        },
    }
}

We implement this same functionality in TypeScript:

import { openSync, writeFileSync } from 'fs';

const ENOENT = 'ENOENT';
const FILE_NAME = './hello.txt';

const openOrCreateFile = (): number => {
  try {
    return openSync(FILE_NAME, 'r');
  } catch (err) {
    if (err.code === ENOENT) {
      writeFileSync(FILE_NAME, '');
      return openSync(FILE_NAME, 'r');
    }
    throw err;
  }
};

const fd = openOrCreateFile();

Observations:

  • First, TypeScript, unlike Rust, does not require us to handle the errors; we have to remember to handle them
  • Unlike Rust with the more general approach using enums, TypeScript has a special syntax, try catch, used specifically for error handling

Generic Data Types

Let us walk through the examples in the Rust Book Generic Data Types section and contrast them with TypeScript.

The code for this section is available to download; Rust download and TypeScript download.

Rust’s generic data types, for the most part, is pretty straightforward; assuming you have used them in some other language.

fn main() {
    // IN FUNCTION
    let x = identity(0); // i32
    let y = identity(3.0); // f64

    // IN STRUCT
    let integer = Point { x: 5, y: 10 }; // Point<i32>
    let float = Point { x: 1.0, y: 4.0 }; // Point<f64>

    // IN METHOD
    let integer_x = integer.x(); // &i32
    let float_x = float.x(); // &f64

    // IN METHOD SPECIFIC TYPE
    let d = float.distance_from_origin(); // f64
}

fn identity<T>(item: T) -> T {
    item
}

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

We implement most of the same feature in TypeScript:

const identity = <T>(item: T): T => item;

interface Point<T> {
  x: T;
  y: T;
}

const pointToX = <T>(point: Point<T>): T => point.x;

class PointClass<T> {
  x: T;

  y: T;

  constructor(x: T, y: T) {
    this.x = x;
    this.y = y;
  }

  getX(): T {
    return this.x;
  }
}

// IN FUNCTION
const x = identity(0); // number
const y = identity('hello'); // string

// IN STRUCT
const integer = { x: 5, y: 10 };
const str = { x: 'hello', y: 'world' };
const integerX = pointToX(integer); // number
const strX = pointToX(str); // string

// IN METHOD
const integerClass = new PointClass(5, 10);
const strClass = new PointClass('hello', 'world');
const integerClassX = integerClass.getX(); // number
const strClassX = strClass.getX(); // string

// IN METHOD SPECIFIC TYPE
// NO IDEA HOW TO IMPLMENT IN TYPESCRIPT

Observations:

  • Could not think how to cleanly implement the equivalent of the type-specific method, distance_from_origin, in TypeScript. The closest that I think we could come would be to create a method that would throw a run-time error if used with an incorrect type (this felt wrong)

Let us walk through the examples in the Rust Book Error Handlingsection and contrast them with TypeScript.

The code for this section is available to download; Rust download and TypeScript download.

Next Steps

Going to take a break for a week or so; my head is full…

#Rust #TypeScript #javascript #web-development #angular

Learning Rust with TypeScript
37.75 GEEK