Merge branch 'main' of git.zekro.de:zekro/type-level-demo

This commit is contained in:
zekro committed 2026-07-26 23:52:33 +02:00
commit ba799bd47b
11 files changed
+179 -53

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ struct CredentialManager {
impl CredentialManager {
pub fn new() -> CredentialManager {
CredentialManager {
locked: false,
locked: true,
entries: HashMap::new(),
}
}
+5 -5
View File
@@ -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();
}
+6 -6
View File
@@ -11,7 +11,7 @@ struct CredentialManager<State = Locked> {
}
impl CredentialManager {
pub fn new() -> CredentialManager<Locked> {
pub fn new() -> CredentialManager {
CredentialManager {
entries: HashMap::new(),
state: std::marker::PhantomData,
@@ -46,9 +46,9 @@ impl CredentialManager<Unlocked> {
}
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();
}
+5 -7
View File
@@ -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()
}
+8
View File
@@ -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()
}
-34
View File
@@ -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<TOp extends OpCode, TArgs extends OpArgsMapping[TOp]>(
op: TOp,
args: TArgs,
) {
// ...
}
}
function main() {
const client = new WebSocketClient();
client.op(OpCode.HELLO, { name: "foo" });
}
+30
View File
@@ -0,0 +1,30 @@
type State = "locked" | "unlocked";
class CredentialManager<S extends State> {
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();
}
+36
View File
@@ -0,0 +1,36 @@
enum OpCode {
PING,
HELLO,
MOVE,
}
type PingEvent = Record<string, never>;
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<TOp extends OpCode, TArgs extends OpArgsMapping[TOp]>(
op: TOp,
args: TArgs,
) {
// ...
}
}
function main1() {
const client = new WebSocketClient();
client.send(OpCode.PING, {});
}
+30
View File
@@ -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> = Url extends `${infer Start}/${infer Rest}`
? ParseUrlParamsRaw<Start> & ParseUrlParamsRaw<Rest>
: 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<Url> is `{}`. `keyof {}` evaluates to `never`, so
// when `{}` is evaluated an empty Record is expected, otherwise the result of
// ParseUrlParamsRaw is expected.
type ParseUrlParams<Url> = keyof ParseUrlParamsRaw<Url> extends never
? Record<string, never>
: ParseUrlParamsRaw<Url>;
function navigate<P extends string>(path: P, params: ParseUrlParams<P>) {
// ...
}
function main3() {
navigate("/hello", {});
navigate("/hello/{name}", { name: "foo" });
navigate("/book/{author}/{id}", { author: "foo", id: "bar" });
}
+1
View File
@@ -1,3 +1,4 @@
[workspace]
resolver = "3"
members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"]
resolver = "3"
+57
View File
@@ -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/