Learning Rust by Contrasting with TypeScript: Part 11
A surprising deep dive into Rust lifetimes.

This article is part of a series starting with Learning Rust by Contrasting with TypeScript: Part 1.
Let us walk through the examples in the Rust Book Validating References with Lifetimes section and contrast them with TypeScript.
The code for this section is available to download; Rust download and TypeScript download.
Sidebar into Rust Scopes Versus Lifetimes
So far we have been focusing on variable scope; let us remind ourselves what scope means for Rust variables.
We first learn that a variable is valid from the point where it is defined to the end of the current scope (where the scope is implied to be the closing curly bracket of the enclosing block).
The variable s refers to a string literal, where the value of the string is hardcoded into the text of our program. The variable is valid from the point at which it’s declared until the end of the current scope.
— Rust — What Is Ownership?
Later, the definition of scope (specific to a reference variable) is refined to end when it is last used (but still has to before the closing curly bracket of the enclosing block).
Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used.
— Rust — References and Borrowing
It is not clear if the later definition applies to just reference variables; e.g., does it apply for variables of type i32? I have come to believe that such a distinction is only relevant for reference variables; so for simplicity I am running under the assumption that the definition is the latter.
Now what about lifetimes? My first assumption was that a variable’s lifetime was synonymous to its scope; turns out no it is not.
A lifetime is a construct the compiler (or more specifically, its borrow checker) uses to ensure all borrows are valid. Specifically, a variable’s lifetime begins when it is created and ends when it is destroyed. While lifetimes and scopes are often referred to together, they are not the same.
Take, for example, the case where we borrow a variable via &. The borrow has a lifetime that is determined by where it is declared. As a result, the borrow is valid as long as it ends before the lender is destroyed. However, the scope of the borrow is determined by where the reference is used.
— Rust — Lifetimes
The key here is that a variable’s lifetime starts when it is introduced (same as scope) and ends when the value it refers to is destroyed.
Let us look at a simple example:
Observations:
- x’s scope starts at line 2 and ends on line 4
- x’s lifetime starts at line 2 and ends on line 6
- y’s scope starts and ends on line 4
- y’s lifetime starts on line 4 and ends on line 6
Let us consider a special example with string literals:
Observations:
- x’s scope starts and ends on line 2
- x’s lifetime starts on line 2 and effectively does not have an end; the value it refers to is never destroyed as it is part of the binary
Interestingly, we learn about static lifetimes later in the Rust documentation; glad to see that I guessed correctly here.
One special lifetime we need to discuss is ‘static, which means that this reference can live for the entire duration of the program. All string literals have the ‘static lifetime…
— Rust — Validating References with Lifetimes: Static Lifetimes
Sidebar into Uninitialized Variables
With Rust, we cannot use uninitialized variables; even simply trying to print them, the code will fail to compile.
We have a mixed result with TypeScript; seems like a hot mess to me.
Observations:
- We are able to use undefined variables in string template literals
- We cannot perform operations, like addition with them
- But, we can pass it to a function that itself performs addition
Sidebar into Implicit Typing
With Rust, even if we do not explicitly provide a type, it will properly implicitly type variables; in the following example r is of type i32.
With TypeScript, in some cases, not supplying a type results in an any type which side-steps type checking. Yuck.
Preventing Dangling References with Lifetimes
The Borrow Checker
The key to understanding the borrow checker, and whether code will compile, is that a variables scope cannot exceed its lifetime. The following example would not compile if uncommented as it breaks this rule:
Observations:
- r’s scope start on line 3 and ends on line 8
- r’s lifetime starts on line 3 and ends on line 7
Before we look at a similar example with TypeScript, let us remind ourselves of a couple of things.
Unlike Rust, TypeScript variables are block-scoped, i.e, scoped to their nearest containing block (the scope).
When a variable is declared using let, it uses what some call lexical-scoping or block-scoping. Unlike variables declared with var whose scopes leak out to their containing function, block-scoped variables are not visible outside of their nearest containing block
— TypeScript — Variable Declarations
note: In modern JavaScript and TypeScript, we do not use var.
Also, we remember that in TypeScript the garbage collector only deallocates objects when there are no remaining references to them. This means that the concept of variable lifetimes is not relevant in TypeScript.
With all this in mind, let us look at a comparable TypeScript example:
Observations:
- The key here is that on line 4, the variable r is set to reference the object that x referenced (not x itself). Now because r references the object, the garbage collector will not deallocate the object before line 6
Generic Lifetimes in Functions
Lifetime Annotation Syntax
Lifetime Annotations in Function Signatures
Thinking in Terms of Lifetimes
The key to understanding this section is understand that lifetimes are different than scopes; once I understood this, this material made a lot more sense.
One bit of confusion is over the precise meaning of the annotation constraint.
The constraint we want to express in this signature is that all the references in the parameters and the return value must have the same lifetime
— Rust — Validating References with Lifetimes: Lifetime Annotations in Function Signatures
By going through a number of the examples in the articles, seems that the more precise statement is that the return value’s lifetime end is set to the minimum lifetime end of all the annotated parameters.
Let use look at an example that compiles.
Observations:
- string2 scope (4 => 5), lifetime (4 => infinity)
- string1.as_str() scope (5 => 5), lifetime (5 => 8)
- result scope (5 => 6), lifetime (5 => 8); set with minimum end of 8
- The key here is that in all cases, the scope end is before the lifetime end
Now let us look at
Observations:
- string2 scope (6 => 7), lifetime (6 => 8)
- string1.as_str(): scope (7 => 7), lifetime (7 => 11)
- result: scope (5 => 9), lifetime (7 => 8); set minimum end 8
- The key here is that for result the lifetime ends before the scope does; so does not compile
With TypeScript, we only need to worry about scope as lifetimes do not exist; the following example is fine.
Lifetime Annotations in Struct Definitions
Lifetime Elision
Lifetime Annotations in Method Definitions
The remaining sections of the material on lifetimes cover some additional scenarios where we are required (or not) use lifetime annotations.
Rather than continuing to repeat ourselves, will simply remind ourselves that such lifetime problems are not relevant in TypeScript because simply having a reference to an object guarantees that the garbage collector will not deallocate it.
Next Steps
We continue by learning about Rust closures in Learning Rust by Contrasting with TypeScript: Part 12.