Skip to content

Commit 9303085

Browse files
authored
Merge pull request #342 from milkyskies/feature/#340.unified-type-syntax
[#340] Unify type definition syntax: use {} for all type fields
2 parents 69e0650 + 4c8229c commit 9303085

38 files changed

Lines changed: 524 additions & 363 deletions

docs/design.md

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now
6767
| `?` operator | `fetchUser(id)?` | early return on Err/None |
6868
| Branded types | `type UserId = Brand<string, "UserId">` | `string` at runtime |
6969
| Opaque types | `opaque type HashedPw = string` | `string`, but only the defining module can create/read |
70-
| Tagged unions | `type Route = Home \| Profile(id: string)` | discriminated union |
70+
| Tagged unions | `type Route { \| Home \| Profile { id: string } }` | discriminated union |
7171
| String literal unions | `type Method = "GET" \| "POST" \| "PUT"` | `"GET" \| "POST" \| "PUT"` (pass-through for npm interop) |
72-
| Nested unions | `type ApiError = Network(NetworkError) \| NotFound` | nested discriminated union (compiler generates tags) |
72+
| Nested unions | `type ApiError { \| Network { NetworkError } \| NotFound }` | nested discriminated union (compiler generates tags) |
7373
| Multi-depth match | `Network(Timeout(ms)) -> ...` | nested if/else with tag checks |
7474
| Type constructors | `User(name: "Ryan", email: e)` | `{ name: "Ryan", email: e }` (compiler adds tags for unions) |
7575
| Record spread | `User(..user, name: "New")` | `{ ...user, name: "New" }` |
@@ -383,7 +383,7 @@ The return type of `collect { ... }` is `Result<T, Array<E>>` where:
383383
### Option<T> - No Null, No Undefined
384384

385385
```floe
386-
type User = {
386+
type User {
387387
name: string // always present
388388
nickname: Option<string> // might not exist
389389
avatar: Option<Url> // might not exist
@@ -411,86 +411,95 @@ const avatar = user.nickname |> Option.flatMap(fn(n) findAvatar(n))
411411
`type` does everything. No `|` = record. Has `|` = union. Unions nest infinitely.
412412

413413
```floe
414-
// Record type (no |)
415-
type User = {
414+
// Record type
415+
type User {
416416
id: UserId
417417
name: string
418418
email: Email
419419
}
420420
421421
// Record type composition with spread
422-
type BaseProps = {
422+
type BaseProps {
423423
className: string,
424424
disabled: boolean,
425425
}
426426
427-
type ButtonProps = {
427+
type ButtonProps {
428428
...BaseProps,
429429
onClick: fn() -> (),
430430
label: string,
431431
}
432432
// ButtonProps has: className, disabled, onClick, label
433433
434434
// Multiple spreads
435-
type A = { x: number }
436-
type B = { y: string }
437-
type C = { ...A, ...B, z: boolean }
435+
type A { x: number }
436+
type B { y: string }
437+
type C { ...A, ...B, z: boolean }
438438
// C has: x, y, z
439439
440440
// Simple union type (has |)
441-
type Route =
441+
type Route {
442442
| Home
443-
| Profile(id: string)
444-
| Settings(tab: string)
443+
| Profile { id: string }
444+
| Settings { tab: string }
445445
| NotFound
446+
}
446447
447448
// String literal union (for npm interop)
448449
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
449450
450451
// Union types can contain other union types — nest as deep as you want
451-
type NetworkError =
452-
| Timeout(ms: number)
453-
| DnsFailure(host: string)
454-
| ConnectionRefused(port: number)
452+
type NetworkError {
453+
| Timeout { ms: number }
454+
| DnsFailure { host: string }
455+
| ConnectionRefused { port: number }
456+
}
455457
456-
type ValidationError =
457-
| Required(field: string)
458-
| InvalidFormat(field: string, expected: string)
459-
| TooLong(field: string, max: number)
458+
type ValidationError {
459+
| Required { field: string }
460+
| InvalidFormat { field: string, expected: string }
461+
| TooLong { field: string, max: number }
462+
}
460463
461-
type AuthError =
464+
type AuthError {
462465
| InvalidCredentials
463-
| TokenExpired(expiredAt: Date)
464-
| InsufficientRole(required: Role, actual: Role)
466+
| TokenExpired { expiredAt: Date }
467+
| InsufficientRole { required: Role, actual: Role }
468+
}
465469
466470
// Parent union containing sub-unions
467-
type ApiError =
468-
| Network(NetworkError)
469-
| Validation(ValidationError)
470-
| Auth(AuthError)
471+
type ApiError {
472+
| Network { NetworkError }
473+
| Validation { ValidationError }
474+
| Auth { AuthError }
471475
| NotFound
472-
| ServerError(status: number, body: string)
476+
| ServerError { status: number, body: string }
477+
}
473478
474479
// Go deeper — a full app error hierarchy
475-
type HttpError =
476-
| Network(NetworkError)
477-
| Status(code: number, body: string)
478-
| Decode(JsonError)
479-
480-
type UserError =
481-
| Http(HttpError)
482-
| NotFound(id: UserId)
483-
| Banned(reason: string)
484-
485-
type PaymentError =
486-
| Http(HttpError)
487-
| InsufficientFunds(needed: number, available: number)
488-
| CardDeclined(reason: string)
489-
490-
type AppError =
491-
| User(UserError)
492-
| Payment(PaymentError)
493-
| Auth(AuthError)
480+
type HttpError {
481+
| Network { NetworkError }
482+
| Status { code: number, body: string }
483+
| Decode { JsonError }
484+
}
485+
486+
type UserError {
487+
| Http { HttpError }
488+
| NotFound { id: UserId }
489+
| Banned { reason: string }
490+
}
491+
492+
type PaymentError {
493+
| Http { HttpError }
494+
| InsufficientFunds { needed: number, available: number }
495+
| CardDeclined { reason: string }
496+
}
497+
498+
type AppError {
499+
| User { UserError }
500+
| Payment { PaymentError }
501+
| Auth { AuthError }
502+
}
494503
```
495504

496505
### Multi-Depth Matching
@@ -625,7 +634,7 @@ Records and functions use the same call syntax: `Name(args)` with optional label
625634
```floe
626635
// --- Record Construction ---
627636
628-
type User = {
637+
type User {
629638
id: UserId
630639
name: string
631640
email: Email
@@ -669,7 +678,7 @@ createUser("Ryan", role: Admin, email: Email("r@test.com"))
669678
// --- Default Values ---
670679
671680
// On record types
672-
type Config = {
681+
type Config {
673682
baseUrl: string // required — no default
674683
timeout: number = 5000 // default value
675684
retries: number = 3 // default value
@@ -697,7 +706,7 @@ fetchUsers(page: 3) // override one
697706
fetchUsers(limit: 50, sort: Descending) // override two
698707
699708
// On React component props
700-
type ButtonProps = {
709+
type ButtonProps {
701710
label: string // required
702711
onClick: fn() -> () // required
703712
variant: Variant = Primary // default
@@ -732,7 +741,7 @@ Two syntactic forms are supported:
732741
**Block form** — group multiple functions:
733742

734743
```floe
735-
type User = { name: string, age: number, active: bool }
744+
type User { name: string, age: number, active: bool }
736745
737746
for User {
738747
fn display(self) -> string {
@@ -851,7 +860,7 @@ Trait rules:
851860
Record types can auto-derive trait implementations with `deriving`:
852861

853862
```floe
854-
type User = {
863+
type User {
855864
id: string,
856865
name: string,
857866
email: string,
@@ -956,13 +965,17 @@ fn double(x: number) -> number { x * 2 } // correct
956965
```floe
957966
import { useState } from "react"
958967
959-
type Todo = {
968+
type Todo {
960969
id: string
961970
text: string
962971
done: boolean
963972
}
964973
965-
type Tab = Overview | Team | Analytics
974+
type Tab {
975+
| Overview
976+
| Team
977+
| Analytics
978+
}
966979
967980
export fn Dashboard(userId: UserId) -> JSX.Element {
968981
const [tab, setTab] = useState<Tab>(Overview)
@@ -1611,7 +1624,7 @@ fn deleteUser(id: UserId) -> Result<(), ApiError> {
16111624
}
16121625
16131626
// Callbacks
1614-
type ButtonProps = {
1627+
type ButtonProps {
16151628
onClick: fn() -> ()
16161629
}
16171630
```
@@ -1717,6 +1730,7 @@ const c = { ...a, ...b } // WARNING: 'y' from 'a' is overwritten by 'b'
17171730
| Spread overlap | Warning on statically-known key overlap | Catches silent overwrites at compile time |
17181731
| Compiler language | Rust | Fast, WASM-ready for browser playground, good LSP story |
17191732
| Inline tests | `test "name" { assert expr }` co-located with code | Gleam/Rust-inspired; type-checked always, stripped from production output |
1733+
| Type definitions | `type Foo { fields }` for records, `type Foo { \| A \| B }` for unions | Unified syntax: all nominal types use `type Name { ... }`. `=` only for aliases and string literal unions |
17201734
| For blocks | `for Type { fn f(self) ... }` groups functions under a type | Rust/Swift-like method chaining DX without OOP. `self` is explicit, no `this` magic |
17211735

17221736
---

docs/llms.txt

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,56 +51,63 @@ const names: Array<string> = ["a", "b", "c"]
5151

5252
```floe
5353
// Record type
54-
type User = {
54+
type User {
5555
id: string,
5656
name: string,
5757
email: string,
5858
}
5959

6060
// Union type (discriminated)
61-
type Route =
61+
type Route {
6262
| Home
63-
| Profile(id: string)
64-
| Settings(tab: string)
63+
| Profile { id: string }
64+
| Settings { tab: string }
6565
| NotFound
66+
}
6667

6768
// Union types can carry data
68-
type Validation =
69-
| Valid(text: string)
69+
type Validation {
70+
| Valid { text: string }
7071
| TooShort
7172
| TooLong
7273
| Empty
74+
}
7375

7476
// Nested unions
75-
type NetworkError =
76-
| Timeout(ms: number)
77-
| DnsFailure(host: string)
77+
type NetworkError {
78+
| Timeout { ms: number }
79+
| DnsFailure { host: string }
80+
}
7881

79-
type ApiError =
80-
| Network(NetworkError)
82+
type ApiError {
83+
| Network { NetworkError }
8184
| NotFound
82-
| ServerError(status: number)
85+
| ServerError { status: number }
86+
}
87+
88+
// Newtype (single-value wrapper)
89+
type OrderId { number }
8390

8491
// String literal union (for npm interop)
8592
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
8693

8794
// Record type composition with spread
88-
type BaseProps = {
95+
type BaseProps {
8996
className: string,
9097
disabled: boolean,
9198
}
9299

93-
type ButtonProps = {
100+
type ButtonProps {
94101
...BaseProps,
95102
onClick: fn() -> (),
96103
label: string,
97104
}
98105
// ButtonProps has: className, disabled, onClick, label
99106

100107
// Multiple spreads
101-
type A = { x: number }
102-
type B = { y: string }
103-
type C = { ...A, ...B, z: boolean }
108+
type A { x: number }
109+
type B { y: string }
110+
type C { ...A, ...B, z: boolean }
104111

105112
// Branded types
106113
type UserId = Brand<string, "UserId">
@@ -273,7 +280,7 @@ fn loadProfile(id: string) -> Result<Profile, Error> {
273280
}
274281

275282
// Option<T> replaces null/undefined
276-
type User = {
283+
type User {
277284
name: string,
278285
nickname: Option<string>,
279286
}
@@ -410,7 +417,7 @@ for User: Display {
410417
}
411418

412419
// Auto-derive traits for record types (only Display — Eq is built-in via ==)
413-
type Point = {
420+
type Point {
414421
x: number,
415422
y: number,
416423
} deriving (Display)
@@ -422,7 +429,7 @@ type Point = {
422429
```floe
423430
import trusted { useState } from "react"
424431

425-
type Todo = {
432+
type Todo {
426433
id: string,
427434
text: string,
428435
done: boolean,
@@ -507,7 +514,7 @@ match key {
507514
| `null`, `undefined` | `Option<T>` with `Some`/`None` |
508515
| `let`, `var` | `const` |
509516
| `class` | Functions + types |
510-
| `enum` | `type` with `\|` variants |
517+
| `enum` | `type Name { \| A \| B }` variants |
511518
| `throw` | Return `Result<T, E>` |
512519
| `if`/`else` | `match` expression |
513520
| `? :` (ternary) | `match` expression |

docs/site/src/content/docs/guide/for-blocks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ title: For Blocks
77
## Basic Usage
88

99
```floe
10-
type User = { name: string, age: number }
10+
type User { name: string, age: number }
1111
1212
for User {
1313
fn display(self) -> string {

docs/site/src/content/docs/guide/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Floe removes these and adds features that make correct code easy to write:
2222
```floe
2323
import trusted { useState } from "react"
2424
25-
type Todo = {
25+
type Todo {
2626
id: string,
2727
text: string,
2828
done: boolean,

docs/site/src/content/docs/guide/jsx.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Components are exported `fn` declarations with a `JSX.Element` return type. The
2424
## Props
2525

2626
```floe
27-
type ButtonProps = {
27+
type ButtonProps {
2828
label: string,
2929
onClick: fn() -> (),
3030
disabled: boolean,

0 commit comments

Comments
 (0)