From b86c9d915ea558a1897ef283c0d1f9005749f4c2 Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Mon, 27 Jul 2026 11:06:19 +0200 Subject: [PATCH] add more documentation --- 03-builder-pattern/README.md | 4 ++++ 04-typestate/README.md | 7 +++++++ 05-typestate-go-structs/README.md | 23 +++++++++++++++++++++++ 06-typestate-go-interfaces/README.md | 23 +++++++++++++++++++++++ 07-typestate-ts/README.md | 27 +++++++++++++++++++++++++++ 08-type-mapping/README.md | 27 +++++++++++++++++++++++++++ 09-type-programming/README.md | 18 ++++++++++++++++++ Cargo.toml | 1 - 8 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 05-typestate-go-structs/README.md create mode 100644 06-typestate-go-interfaces/README.md create mode 100644 07-typestate-ts/README.md create mode 100644 08-type-mapping/README.md create mode 100644 09-type-programming/README.md diff --git a/03-builder-pattern/README.md b/03-builder-pattern/README.md index d98679a..80e0608 100644 --- a/03-builder-pattern/README.md +++ b/03-builder-pattern/README.md @@ -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. diff --git a/04-typestate/README.md b/04-typestate/README.md index e69de29..45de4ad 100644 --- a/04-typestate/README.md +++ b/04-typestate/README.md @@ -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`](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. diff --git a/05-typestate-go-structs/README.md b/05-typestate-go-structs/README.md new file mode 100644 index 0000000..70c5dba --- /dev/null +++ b/05-typestate-go-structs/README.md @@ -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! 💥 +``` diff --git a/06-typestate-go-interfaces/README.md b/06-typestate-go-interfaces/README.md new file mode 100644 index 0000000..ec46d09 --- /dev/null +++ b/06-typestate-go-interfaces/README.md @@ -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 +``` diff --git a/07-typestate-ts/README.md b/07-typestate-ts/README.md new file mode 100644 index 0000000..8b4265a --- /dev/null +++ b/07-typestate-ts/README.md @@ -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 ...`). diff --git a/08-type-mapping/README.md b/08-type-mapping/README.md new file mode 100644 index 0000000..b3cadec --- /dev/null +++ b/08-type-mapping/README.md @@ -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( + 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 +``` diff --git a/09-type-programming/README.md b/09-type-programming/README.md new file mode 100644 index 0000000..9409cdd --- /dev/null +++ b/09-type-programming/README.md @@ -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 = ...; + +function navigate

(path: P, params: ParseUrlParams

) { + // ... +} + +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. diff --git a/Cargo.toml b/Cargo.toml index a2c3360..319ecf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ [workspace] resolver = "3" members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"] -resolver = "3"