diff --git a/02-runtime-check/src/main.rs b/02-runtime-check/src/main.rs index 70bfc3d..37e9098 100644 --- a/02-runtime-check/src/main.rs +++ b/02-runtime-check/src/main.rs @@ -10,7 +10,7 @@ struct CredentialManager { impl CredentialManager { pub fn new() -> CredentialManager { CredentialManager { - locked: false, + locked: true, entries: HashMap::new(), } } diff --git a/03-builder-pattern/src/main.rs b/03-builder-pattern/src/main.rs index e6269d9..51cff0d 100644 --- a/03-builder-pattern/src/main.rs +++ b/03-builder-pattern/src/main.rs @@ -41,9 +41,9 @@ impl UnlockedCredentialManager { } fn main() { - let pw = LockedCredentialManager::new(); - let mut pw = pw.unlock("pssst"); - pw.set("foo", "bar"); - let val = pw.get("foo"); - pw.lock(); + let pw_locked = LockedCredentialManager::new(); + let mut pw_unlocked = pw_locked.unlock("pssst"); + pw_unlocked.set("foo", "bar"); + let val = pw_unlocked.get("foo"); + let pw_locked = pw_unlocked.lock(); } diff --git a/04-typestate/src/main.rs b/04-typestate/src/main.rs index fd6cec9..f6c9967 100644 --- a/04-typestate/src/main.rs +++ b/04-typestate/src/main.rs @@ -11,7 +11,7 @@ struct CredentialManager { } impl CredentialManager { - pub fn new() -> CredentialManager { + pub fn new() -> CredentialManager { CredentialManager { entries: HashMap::new(), state: std::marker::PhantomData, @@ -46,9 +46,9 @@ impl CredentialManager { } fn main() { - let pw = CredentialManager::new(); - let mut pw = pw.unlock("pssst"); - pw.set("foo", "bar"); - let val = pw.get("foo"); - pw.lock(); + let pw_locked = CredentialManager::new(); + let mut pw_unlocked = pw_locked.unlock("pssst"); + pw_unlocked.set("foo", "bar"); + let val = pw_unlocked.get("foo"); + let pw_locked = pw_unlocked.lock(); } diff --git a/05-typestate-go-structs/main.go b/05-typestate-go-structs/main.go index 6957835..18bbcbd 100644 --- a/05-typestate-go-structs/main.go +++ b/05-typestate-go-structs/main.go @@ -35,11 +35,9 @@ func (m UnlockedCredentialManager) Lock() LockedCredentialManager { } func main() { - pw := NewCredentialManager() - - unlocked := pw.Unlock("pssst") - unlocked.Set("foo", "bar") - _, _ = unlocked.Get("foo") - - unlocked.Lock() + pwLocked := NewCredentialManager() + pwUnlocked := pwLocked.Unlock("pssst") + pwUnlocked.Set("foo", "bar") + _, _ = pwUnlocked.Get("foo") + pwUnlocked.Lock() } diff --git a/06-typestate-go-interfaces/main.go b/06-typestate-go-interfaces/main.go index 13b5719..06d453c 100644 --- a/06-typestate-go-interfaces/main.go +++ b/06-typestate-go-interfaces/main.go @@ -52,3 +52,11 @@ func (t *credentialManager) Get(key string) (string, error) { } panic("todo") } + +func main() { + pwLocked := NewCredentialManager() + pwUnlocked, _ := pwLocked.Unlock("pssst") + pwUnlocked.Set("foo", "bar") + _, _ = pwUnlocked.Get("foo") + pwUnlocked.Lock() +} diff --git a/07-type-mapping/main.ts b/07-type-mapping/main.ts deleted file mode 100644 index c1ac3cd..0000000 --- a/07-type-mapping/main.ts +++ /dev/null @@ -1,34 +0,0 @@ -enum OpCode { - PING, - HELLO, - MOVE, -} - -type HelloEvent = { - name: string; -}; - -type MoveEvent = { - x: number; - y: number; -}; - -type OpArgsMapping = { - [OpCode.PING]: null; - [OpCode.HELLO]: HelloEvent; - [OpCode.MOVE]: MoveEvent; -}; - -class WebSocketClient { - public op( - op: TOp, - args: TArgs, - ) { - // ... - } -} - -function main() { - const client = new WebSocketClient(); - client.op(OpCode.HELLO, { name: "foo" }); -} diff --git a/07-typestate-ts/main.ts b/07-typestate-ts/main.ts new file mode 100644 index 0000000..bf032e8 --- /dev/null +++ b/07-typestate-ts/main.ts @@ -0,0 +1,30 @@ +type State = "locked" | "unlocked"; + +class CredentialManager { + private readonly state?: S; + + unlock(this: CredentialManager<"locked">, master_key: string) { + return this as unknown as CredentialManager<"unlocked">; + } + + set(this: CredentialManager<"unlocked">, key: string, value: any) { + // ... + } + + get(this: CredentialManager<"unlocked">, key: string): string | undefined { + // ... + return undefined; + } + + lock(this: CredentialManager<"unlocked">): CredentialManager<"locked"> { + return this as unknown as CredentialManager<"locked">; + } +} + +function main2() { + 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(); +} diff --git a/08-type-mapping/main.ts b/08-type-mapping/main.ts new file mode 100644 index 0000000..f5dccb4 --- /dev/null +++ b/08-type-mapping/main.ts @@ -0,0 +1,36 @@ +enum OpCode { + PING, + HELLO, + MOVE, +} + +type PingEvent = Record; + +type HelloEvent = { + name: string; +}; + +type MoveEvent = { + x: number; + y: number; +}; + +type OpArgsMapping = { + [OpCode.PING]: PingEvent; + [OpCode.HELLO]: HelloEvent; + [OpCode.MOVE]: MoveEvent; +}; + +class WebSocketClient { + public send( + op: TOp, + args: TArgs, + ) { + // ... + } +} + +function main1() { + const client = new WebSocketClient(); + client.send(OpCode.PING, {}); +} diff --git a/09-type-programming/main.ts b/09-type-programming/main.ts new file mode 100644 index 0000000..17a5415 --- /dev/null +++ b/09-type-programming/main.ts @@ -0,0 +1,30 @@ +// This recursively matches path segments and parameters in segments which +// are surrounded by curly braces (i.e. `{foo}`) in these segments. +// The results are joined to a superset of objects with their params +// via the `&`. +// In example, the path /{foo}/{bar} results in the type +// `{ foo: string } & { bar: string }` +type ParseUrlParamsRaw = Url extends `${infer Start}/${infer Rest}` + ? ParseUrlParamsRaw & ParseUrlParamsRaw + : Url extends `{${infer Param}}` + ? { [P in Param]: string } + : {}; + +// ParseUrlParamsRaw allows passing objects with any keys (i.e. `{name: "foo"}`) even though +// an empty object is expected (`{}`). This type constraint enforces this by checking if +// the result of ParseUrlParamsRaw is `{}`. `keyof {}` evaluates to `never`, so +// when `{}` is evaluated an empty Record is expected, otherwise the result of +// ParseUrlParamsRaw is expected. +type ParseUrlParams = keyof ParseUrlParamsRaw extends never + ? Record + : ParseUrlParamsRaw; + +function navigate

(path: P, params: ParseUrlParams

) { + // ... +} + +function main3() { + navigate("/hello", {}); + navigate("/hello/{name}", { name: "foo" }); + navigate("/book/{author}/{id}", { author: "foo", id: "bar" }); +} diff --git a/Cargo.toml b/Cargo.toml index 319ecf7..a2c3360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ [workspace] resolver = "3" members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"] +resolver = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..89de3de --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Type Level Programming + +This repository contains a collection of examples demonstrating various type-level programming patterns in Rust, Go, and TypeScript. + +The goal of these examples is to show how to leverage the type system of a language to enforce invariants at compile-time, reducing the need for runtime checks and making the code more robust and self-documenting. + +## Repository Structure + +The examples are organized by language and complexity. + +### Rust + +The Rust examples demonstrate a progression of state management for a `CredentialManager`: + +- **[01-problem](./01-problem)**: The initial problem where state is not managed, allowing operations in any order. +- **[02-runtime-check](./02-runtime-check)**: Adding runtime checks (e.g., `bool` flags) to ensure operations are performed in the correct state. +- **[03-builder-pattern](./03-builder-pattern)**: Using separate structs to represent different states, ensuring state transitions are enforced at compile-time. +- **[04-typestate](./04-typestate)**: A classic implementation of the Typestate pattern using generics and `PhantomData` to track state. + +### Go + +- **[05-typestate-go-structs](./05-typestate-go-structs)**: Implementing state transitions in Go using separate structs for each state. +- **[06-typestate-go-interfaces](./06-typestate-go-interfaces)**: Using interfaces to restrict the available methods based on the current state of the object. + +### TypeScript + +- **[07-typestate-ts](./07-typestate-ts)**: Implementing the Typestate pattern in TypeScript using `this` type parameters to restrict method calls. +- **[08-type-mapping](./08-type-mapping)**: Mapping enum values (like opcodes) to specific argument types for type-safe messaging. +- **[09-type-programming](./09-type-programming)**: Advanced recursive type parsing to extract parameters from URL string literals at compile-time. + +## Getting Started + +### Rust +You can run the Rust examples using Cargo from the root directory: +```bash +# Example: Running the typestate example +cargo run -p 04-typestate +``` + +### Go +The Go examples can be run using the Go CLI: +```bash +go run ./05-typestate-go-structs +``` + +### TypeScript +The TypeScript examples are best viewed in an IDE (like VS Code) to see the type errors and completions. You can also run them using `ts-node`: +```bash +npx ts-node 07-typestate-ts/main.ts +``` + +## Sources + +- https://www.emoses.org/posts/resolver-resolved-pattern/ +- https://farazdagi.com/posts/2024-04-07-typestate-pattern/ +- https://youtu.be/_ccDqRTx-JU +- https://type-level-typescript.com/