From 2bc148342d214f494b8aa18d06e28c9183e1123a Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Fri, 17 Jul 2026 10:28:20 +0200 Subject: [PATCH 1/5] Dwip --- 07-type-mapping/main.ts | 36 ++++++++++++++++++------------------ 08-type-programming/main.ts | 13 +++++++++++++ README.md | 8 ++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 08-type-programming/main.ts create mode 100644 README.md diff --git a/07-type-mapping/main.ts b/07-type-mapping/main.ts index c1ac3cd..f6fc7fc 100644 --- a/07-type-mapping/main.ts +++ b/07-type-mapping/main.ts @@ -1,34 +1,34 @@ enum OpCode { - PING, - HELLO, - MOVE, + PING, + HELLO, + MOVE, } type HelloEvent = { - name: string; + name: string; }; type MoveEvent = { - x: number; - y: number; + x: number; + y: number; }; type OpArgsMapping = { - [OpCode.PING]: null; - [OpCode.HELLO]: HelloEvent; - [OpCode.MOVE]: MoveEvent; + [OpCode.PING]: {}; + [OpCode.HELLO]: HelloEvent; + [OpCode.MOVE]: MoveEvent; }; class WebSocketClient { - public op( - op: TOp, - args: TArgs, - ) { - // ... - } + public op( + op: TOp, + args: TArgs, + ) { + // ... + } } -function main() { - const client = new WebSocketClient(); - client.op(OpCode.HELLO, { name: "foo" }); +function main_ws() { + const client = new WebSocketClient(); + client.op(OpCode.PING, {}); } diff --git a/08-type-programming/main.ts b/08-type-programming/main.ts new file mode 100644 index 0000000..a057bdf --- /dev/null +++ b/08-type-programming/main.ts @@ -0,0 +1,13 @@ +type ParseUrlParams = Url extends `${infer Start}/${infer Rest}` + ? ParseUrlParams & ParseUrlParams + : Url extends `{${infer Param}}` + ? { [P in Param]: string } + : {}; + +function navigate

(path: P, params: ParseUrlParams

) { + // ... +} + +function main() { + navigate("/foo/bar", { foo: "asd" }); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3aa456b --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Type Level Programming + +## 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/ From 9e4e01966009fca5a22ea9bf64c3fbaf46d57c53 Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Fri, 17 Jul 2026 10:52:41 +0200 Subject: [PATCH 2/5] add more typescript examples --- 07-type-mapping/main.ts | 4 +++- 08-type-programming/main.ts | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/07-type-mapping/main.ts b/07-type-mapping/main.ts index f6fc7fc..b7ec098 100644 --- a/07-type-mapping/main.ts +++ b/07-type-mapping/main.ts @@ -4,6 +4,8 @@ enum OpCode { MOVE, } +type PingEvent = Record; + type HelloEvent = { name: string; }; @@ -14,7 +16,7 @@ type MoveEvent = { }; type OpArgsMapping = { - [OpCode.PING]: {}; + [OpCode.PING]: PingEvent; [OpCode.HELLO]: HelloEvent; [OpCode.MOVE]: MoveEvent; }; diff --git a/08-type-programming/main.ts b/08-type-programming/main.ts index a057bdf..97d718f 100644 --- a/08-type-programming/main.ts +++ b/08-type-programming/main.ts @@ -1,13 +1,23 @@ -type ParseUrlParams = Url extends `${infer Start}/${infer Rest}` - ? ParseUrlParams & ParseUrlParams +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 main() { - navigate("/foo/bar", { foo: "asd" }); + navigate("/hello", {}); + navigate("/hello/{name}", { name: "foo" }); } From e42d897d41e58f41f3e6e81dc734d3cde6036035 Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Fri, 17 Jul 2026 11:30:08 +0200 Subject: [PATCH 3/5] update examples and add documentation --- 02-runtime-check/src/main.rs | 2 +- 03-builder-pattern/src/main.rs | 10 +++++----- 04-typestate/src/main.rs | 12 ++++++------ 05-typestate-go-structs/main.go | 12 +++++------- 06-typestate-go-interfaces/main.go | 8 ++++++++ 07-type-mapping/main.ts | 4 ++-- 08-type-programming/main.ts | 7 +++++++ 7 files changed, 34 insertions(+), 21 deletions(-) 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 index b7ec098..90030b5 100644 --- a/07-type-mapping/main.ts +++ b/07-type-mapping/main.ts @@ -22,7 +22,7 @@ type OpArgsMapping = { }; class WebSocketClient { - public op( + public send( op: TOp, args: TArgs, ) { @@ -32,5 +32,5 @@ class WebSocketClient { function main_ws() { const client = new WebSocketClient(); - client.op(OpCode.PING, {}); + client.send(OpCode.PING, {}); } diff --git a/08-type-programming/main.ts b/08-type-programming/main.ts index 97d718f..4ada33a 100644 --- a/08-type-programming/main.ts +++ b/08-type-programming/main.ts @@ -1,3 +1,9 @@ +// 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}}` @@ -20,4 +26,5 @@ function navigate

(path: P, params: ParseUrlParams

) { function main() { navigate("/hello", {}); navigate("/hello/{name}", { name: "foo" }); + navigate("/book/{author}/{id}", { author: "foo", id: "bar" }); } From 1d9a7fa4fedd2616358b70c383eb8397017af34b Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Mon, 20 Jul 2026 09:54:35 +0200 Subject: [PATCH 4/5] add typestate typescript example --- 07-typestate-ts/main.ts | 30 +++++++++++++++++++ {07-type-mapping => 08-type-mapping}/main.ts | 2 +- .../main.ts | 2 +- Cargo.toml | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 07-typestate-ts/main.ts rename {07-type-mapping => 08-type-mapping}/main.ts (96%) rename {08-type-programming => 09-type-programming}/main.ts (98%) 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/07-type-mapping/main.ts b/08-type-mapping/main.ts similarity index 96% rename from 07-type-mapping/main.ts rename to 08-type-mapping/main.ts index 90030b5..f5dccb4 100644 --- a/07-type-mapping/main.ts +++ b/08-type-mapping/main.ts @@ -30,7 +30,7 @@ class WebSocketClient { } } -function main_ws() { +function main1() { const client = new WebSocketClient(); client.send(OpCode.PING, {}); } diff --git a/08-type-programming/main.ts b/09-type-programming/main.ts similarity index 98% rename from 08-type-programming/main.ts rename to 09-type-programming/main.ts index 4ada33a..17a5415 100644 --- a/08-type-programming/main.ts +++ b/09-type-programming/main.ts @@ -23,7 +23,7 @@ function navigate

(path: P, params: ParseUrlParams

) { // ... } -function main() { +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 222e172..935cde4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"] +resolver = "3" From 88661ec96036affa18f98b9666b1c99adca62789 Mon Sep 17 00:00:00 2001 From: zekroTJA Date: Mon, 20 Jul 2026 09:58:50 +0200 Subject: [PATCH 5/5] update readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 3aa456b..89de3de 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,54 @@ # 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/