add more documentation

This commit is contained in:
zekro committed 2026-07-27 11:32:15 +02:00
1 parent dcef2470cb
commit b86c9d915e
8 files changed
+129 -1

No files matched your search

+4
View File
@@ -49,3 +49,7 @@ note: `UnlockedCredentialManager::lock` takes ownership of the receiver `self`,
36 | pub fn lock(self) -> LockedCredentialManager {
| ^^^^
```
## Caveats
This solution requires two structs for the same functionality which may hurt readability and conciseness of the code. Also, it requires to move data around between struct instances when transitioning between states which introduces unnecessary overhead and complexity.
+7
View File
@@ -0,0 +1,7 @@
# Solution 3: Typestate Pattern using Marker Types
A yet better solution is to utilize the functionality of Rust to be able to do different implementations on a struct depending on type parameters.
We simply introduce two empty marker types `Locked` and `Unlocked`. Then, we pass the marker type as type parameter to the `CredentialManager` struct. Because Rust enforces the usage of type parameters, we need the type parameter to appear in a field for Rust to accept it. We could use `state: Locked` directly, since `Locked` is zero-sized and this compiles fine and costs nothing at runtime. But [`PhantomData<T>`](https://doc.rust-lang.org/nightly/nomicon/phantom-data.html) is preferred by convention because it decouples the type-level marker from needing an actual constructible value of `T`, doesn't silently grow the struct's size if `T` is later changed to something non-zero-sized, and lets you control drop-check/variance/auto-trait behavior independently.
So in this solution, we get the exact same behavior as with the last solution, but with absolutely no extra cost at runtime, which perfectly utilizes the [zero cost abstraction](https://doc.rust-lang.org/beta/embedded-book/static-guarantees/zero-cost-abstractions.html) principle of Rust.
+23
View File
@@ -0,0 +1,23 @@
# Typestate Pattern in Go: Separate Structs
In Go, we can implement the typestate pattern similarly to the Rust "Builder Pattern" solution by using separate structs for each state.
```go
pwLocked := NewCredentialManager()
pwUnlocked := pwLocked.Unlock("pssst")
pwUnlocked.Set("foo", "bar")
_, _ = pwUnlocked.Get("foo")
pwUnlocked.Lock()
```
By returning a different struct type from `Unlock` and `Lock`, we can control which methods are available on the returned object.
## Caveats
Unlike Rust, Go does not have an ownership model. This means that the original object (e.g., `pwLocked`) remains valid and usable even after it has been "moved" to a new state. This can lead to logic errors if the old object is reused.
```go
pwLocked := NewCredentialManager()
pwUnlocked := pwLocked.Unlock("pssst")
pwLocked.Unlock("pssst") // Still possible! 💥
```
+23
View File
@@ -0,0 +1,23 @@
# Typestate Pattern in Go: Interfaces
Another way to implement the typestate pattern in Go is by using interfaces to restrict the methods available on an object based on its current state.
```go
pwLocked := NewCredentialManager()
pwUnlocked, _ := pwLocked.Unlock("pssst")
pwUnlocked.Set("foo", "bar")
_, _ = pwUnlocked.Get("foo")
pwUnlocked.Lock()
```
In this implementation, both `LockedCredentialManager` and `UnlockedCredentialManager` are interfaces that are satisfied by a single internal `credentialManager` struct. The transitions return the same object but cast to the appropriate interface.
## Caveats
Similar to the struct-based approach, interfaces in Go do not prevent the use of the original interface variable after a transition. Additionally, because the underlying type is the same, it's possible to cast the interface back to the original type or another interface if the underlying type is known, bypassing the state protections. It also still requires internal runtime checks to ensure consistency if the object is shared.
```go
pwLocked := NewCredentialManager()
pwUnlocked, _ := pwLocked.Unlock("pssst")
pwUnlocked.(LockedCredentialManager).Unlock("psst") // This is possible and would cause a runtime error
```
+27
View File
@@ -0,0 +1,27 @@
# Typestate Pattern in TypeScript
TypeScript allows for a very elegant implementation of the typestate pattern using `this` type parameters. This enables us to restrict method calls based on the generic state of the class instance.
```typescript
let pw_locked = new CredentialManager<"locked">();
let pw_unlocked = pw_locked.unlock("psst");
pw_unlocked.set("foo", "bar");
let val = pw_unlocked.get("foo");
pw_locked = pw_unlocked.lock();
```
By using `this: CredentialManager<"unlocked">` in the method signature, we tell TypeScript that the method can only be called when the instance is of that specific type.
```typescript
let pw_locked = new CredentialManager<"locked">();
pw_locked.set("foo", "bar"); // -> 💥 Compiler Error
```
This produces a compiler error similar to:
```
Property 'set' does not exist on type 'CredentialManager<"locked">'.
```
## Caveats
Like Go, TypeScript does not have an ownership model, so the original variable remains usable after a transition. Furthermore, TypeScript's type system is structural and can be bypassed with type assertions (`as any` or `as unknown as ...`).
+27
View File
@@ -0,0 +1,27 @@
# Type Mapping in TypeScript
This example demonstrates how to use TypeScript's mapped types and generics to create a type-safe mapping between an operation code (enum) and the expected arguments for that operation.
```typescript
type OpArgsMapping = {
[OpCode.PING]: PingEvent;
[OpCode.HELLO]: HelloEvent;
[OpCode.MOVE]: MoveEvent;
};
class WebSocketClient {
public send<TOp extends OpCode, TArgs extends OpArgsMapping[TOp]>(
op: TOp,
args: TArgs,
) {
// ...
}
}
```
When calling `send`, TypeScript automatically infers the correct type for `args` based on the value passed for `op`.
```typescript
client.send(OpCode.MOVE, { x: 10, y: 20 }); // Correct
client.send(OpCode.MOVE, { name: "foo" }); // -> 💥 Compiler Error
```
+18
View File
@@ -0,0 +1,18 @@
# Type Programming in TypeScript
This example showcases advanced type-level programming in TypeScript using template literal types and recursive type definitions.
We can define a type that parses a string literal at compile-time to extract placeholders and construct a corresponding object type.
```typescript
type ParseUrlParams<Url> = ...;
function navigate<P extends string>(path: P, params: ParseUrlParams<P>) {
// ...
}
navigate("/book/{author}/{id}", { author: "foo", id: "bar" }); // Correct
navigate("/book/{author}/{id}", { author: "foo" }); // -> 💥 Compiler Error: missing 'id'
```
This allows for extremely type-safe APIs, where the parameters required for a function are derived directly from a string template, ensuring that developers provide all necessary data.
-1
View File
@@ -1,4 +1,3 @@
[workspace]
resolver = "3"
members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"]
resolver = "3"