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
Motivation
Watch the video.
The common requirement for both the Rust and TypeScript projects is the GIT version control system.
Rust installation involves installing global binaries, e.g., rustc and cargo. This article was written using version 1.38.0.
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.
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!");
}
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!');
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.
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.
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.
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:
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:
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.
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 code for this article is available to download; Rust download and TypeScript download.
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).
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
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:
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:
note: In Rust, the term reference has a special meaning. That is why we use the more generic term pointer.
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:
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:
note: In TypeScript, const only prevents one from re-assigning the reference value to another object
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:
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:
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.
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:
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:
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:
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:
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]
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.
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:
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:
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.
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
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
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
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]}`);
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).
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.
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:
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:
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:
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.
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:
A TypeScript package can deliver:
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.
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:
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:
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:
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()'");
};
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:
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
});
...
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:
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:
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:
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.
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:
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:
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