Skip to content

Redesign Result type for cleaner Luau inference#1

Merged
Word30210 merged 2 commits into
mainfrom
word30210/fix-result-implementation-types
May 13, 2026
Merged

Redesign Result type for cleaner Luau inference#1
Word30210 merged 2 commits into
mainfrom
word30210/fix-result-implementation-types

Conversation

@Word30210
Copy link
Copy Markdown
Contributor

What

  • Merged Ok<T> and Err<E> into a single Result<T, E> shape that carries both _value: T and _err: E on the same record (instead of two metatable-tagged union variants).
  • Redefined Ok<T> as Result<T, never> and Err<E> as Result<never, E>, so the constructors return precise subtypes that upcast cleanly to Result<T, E>.
  • Updated the ok / err constructors to populate the unused field with nil :: never.
  • Simplified unwrap_unchecked / unwrap_err_unchecked to direct field reads — no more (self :: Ok<T>)._value style downcasts.
  • Removed all :: any and :: Result.Result<T, E> workarounds from tests/std/result/.spec.luau now that subtype-based inference covers every call site.

Why

The previous design used a real metatable-tagged union Ok<T> | Err<E>. Luau's type checker does not narrow setmetatable<...> unions on a boolean discriminator field, so calling generic methods like :is_ok_and(f: (T) -> boolean) on an Err<E> (or the symmetric Err-side methods on Ok<T>) failed type checking with "no valid instantiation for T"_value resolved to nil from the Err branch's missing field and conflicted with the closure's parameter type. As a workaround the tests had to launder these call sites through :: any, which defeated the purpose of having strict types in the first place.

Collapsing the two variants into one record gives both fields a real type at all times, with the unused side parameterized to never. Upcasting an Err<E> = Result<never, E> to Result<T, E> then works through ordinary never <: T covariance on read fields — no narrowing required from the type checker. As a side effect this also lets the unchecked methods read fields directly without a (separately broken) Result<T, E>Ok<T> downcast.

Checklist

Required

  • lute run ./scripts/checker.luau -- ./src ./tests ./scripts passes
  • lute run ./scripts/tester.luau passes

Functional Validation

  • Ok and Err branches still covered for every public method in tests/std/result/.spec.luau
  • unwrap_unchecked / unwrap_err_unchecked exercised on the correct variant
  • Runtime behavior unchanged — refactor is purely type-shape driven

Configuration & Docs

  • No new dependencies; mise.toml / pesde.toml unchanged
  • No sensitive values or credentials were introduced

If Applicable

  • Type-surface change: Ok<T> and Err<E> are now aliases for Result<T, never> / Result<never, E> instead of independent setmetatable shapes. Downstream code that pattern-matched on the previous shape (e.g. discriminating on the absence of _value / _err) would need to switch to checking _ok. Pre-1.0, so impact is expected to be nil.

@Word30210 Word30210 merged commit d461714 into main May 13, 2026
2 checks passed
@Word30210 Word30210 deleted the word30210/fix-result-implementation-types branch May 13, 2026 09:29
@Word30210 Word30210 self-assigned this May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant