Record Spread with Safe Defaults and Controlled Overrides
Summary
This proposal introduces a record spread operator ... inside record literals to support:
- Concise, type-safe copyWith-style updates
- Composability by embedding records into larger records
While enabling both use cases, the design enforces safe defaults:
- Spread operations never implicitly override fields
- Field replacement must be explicit
- The behavior is predictable and stable under API evolution
Design Goals
- Preserve immutability ergonomics (copyWith-style updates)
- Enable record composition (embedding smaller records into larger ones)
- Avoid implicit field overriding
- Maintain strong compile-time guarantees
- Keep syntax minimal and consistent
Core Semantics
1. Spread = structural expansion only
Expands all fields of record into the new record.
2. Multiple spreads concatenate (no implicit override)
- Positional fields: concatenated in order
- Named fields: must not conflict
// Compile-time error if both define `foo`
(...r1, ...r2)
➡️ No silent overriding.
3. Named field override (explicit by position in literal)
(...record, foo: newValue)
- Allowed only if
foo already exists
- Replaces the previously introduced field
// Compile-time error
(...record, unknownField: 123)
4. Positional field override via integer index
- Replaces positional field at index
1
- Only allowed if that field was introduced by a spread
// Compile-time error
(1: newValue) // no prior spread
5. No $n positional syntax
Positional fields are referenced using integer indices, not $1, $2.
Rationale:
- Avoids conflict with valid named fields like
$2
- Keeps syntax aligned with positional semantics
Examples
CopyWith-style update
final state = (isLoading: false, error: null, isSuccess: false);
final newState = (
...state,
isLoading: true,
);
Positional update
final point = (10, 20);
final moved = (
...point,
1: 50,
);
Record composition (embedding)
final point = (x: 10, y: 20);
final colored = (
...point,
color: "red",
);
Combining records
final a = (1, 2);
final b = (3, 4);
final combined = (...a, ...b); // (1, 2, 3, 4)
Conflict detection
final a = (foo: 1);
final b = (foo: 2);
(...a, ...b); // Compile-time error
Rationale
1. Safe by default
- No implicit overriding
- No accidental field shadowing
- Fail-fast behavior
2. Supports both key use cases
| Use case |
Supported |
| copyWith-style updates |
✅ |
| record embedding |
✅ |
| record concatenation |
✅ |
| implicit merging |
❌ |
3. Avoids API evolution hazards
Unlike argument-list spreading:
- No cross-boundary implicit binding
- No silent behavior changes when APIs evolve
- Field usage remains explicit
4. Minimal mental model
... always means expand
- Overriding is explicit and constrained
- No context-dependent semantics
Alternatives Considered
Implicit override (rejected)
(...record, foo: newValue)
where foo may or may not exist.
❌ Problem:
- Silent bugs
- Harder to reason about
- Weakens type guarantees
Explicit override syntax (:=, ...=)
(...record, foo:= newValue)
(...record, ...=otherRecord)
❌ Rejected because:
- Adds new syntax surface
- Increases complexity
- Not necessary if safe constraints exist
Acknowledgement
Parts of this proposal were inspired by community discussion, including suggestions around:
@lrhn
- Using integer indices for positional field updates
- Treating multiple spreads as concatenation rather than override
- Avoiding
$n syntax for positional access
These contributions helped shape a design that balances composability and safety.
Record Spread with Safe Defaults and Controlled Overrides
Summary
This proposal introduces a record spread operator
...inside record literals to support:While enabling both use cases, the design enforces safe defaults:
Design Goals
Core Semantics
1. Spread = structural expansion only
Expands all fields of
recordinto the new record.2. Multiple spreads concatenate (no implicit override)
// Compile-time error if both define `foo` (...r1, ...r2)➡️ No silent overriding.
3. Named field override (explicit by position in literal)
(...record, foo: newValue)fooalready exists4. Positional field override via integer index
15. No
$npositional syntaxPositional fields are referenced using integer indices, not
$1,$2.Rationale:
$2Examples
CopyWith-style update
Positional update
Record composition (embedding)
Combining records
Conflict detection
Rationale
1. Safe by default
2. Supports both key use cases
3. Avoids API evolution hazards
Unlike argument-list spreading:
4. Minimal mental model
...always means expandAlternatives Considered
Implicit override (rejected)
(...record, foo: newValue)where
foomay or may not exist.❌ Problem:
Explicit override syntax (
:=,...=)❌ Rejected because:
Acknowledgement
Parts of this proposal were inspired by community discussion, including suggestions around:
@lrhn
$nsyntax for positional accessThese contributions helped shape a design that balances composability and safety.