tl icon indicating copy to clipboard operation
tl copied to clipboard

Union between 2 tables of the same interface should be able to be indexed

Open Frityet opened this issue 4 months ago • 4 comments

local interface BaseResult<TValue, TError> where self.ok
    ok: boolean

    unwrap: function(self): TValue
end

local record Ok<T> is BaseResult<T, any> where self.ok == true
    value: T
end

local record Error<TError> is BaseResult<any, TError> where self.ok == false
    error: TError
end

function Ok:unwrap(): T
    return self.value
end

function Error:unwrap(): any
    error("Called unwrap on an Error: "..tostring(self.error))
end

local type Result<TValue, TError> = Ok<TValue> | Error<TError>

local function divide(a: number, b: number): Result<number, string>
    if b == 0 then
        return err("Division by zero")
    else
        return ok(a / b)
    end
end

local x = divide(10, 2):unwrap()

This currently fails with cannot index key 'unwrap' in type Result<number, string>

Frityet avatar Oct 21 '25 09:10 Frityet

Union between 2 tables of the same interface

Those two types do not inherit the same interface. Ok inherits BaseResult<T, any> and Error inherits BaseResult<any, TError>. Those are clearly not the same type!

hishamhm avatar Nov 21 '25 16:11 hishamhm

Union between 2 tables of the same interface

Those two types do not inherit the same interface. Ok inherits BaseResult<T, any> and Error inherits BaseResult<any, TError>. Those are clearly not the same type!

In the case it is local type Result<TValue, TError> = Ok<TValue, TError> | Error<TValue, TError> (with proper adjustments to the interfaces) the issue still persists

local interface BaseResult<TValue, TError> where self.ok
    ok: boolean

    unwrap: function(self): TValue
end

local record Ok<T, TError> is BaseResult<T, TError> where self.ok == true
    value: T
end

local record Error<T, TError> is BaseResult<T, TError> where self.ok == false
    error: TError
end

function Ok:unwrap(): T
    return self.value
end

function Error:unwrap(): T
    error("Called unwrap on an Error: "..tostring(self.error))
end

local function ok<TValue, TError>(value: TValue): Ok<TValue, TError>
    return { ok = true, value = value }
end

local function err<TValue, TError>(error: TError): Error<TValue, TError>
    return { ok = false, error = error }
end

local type Result<TValue, TError> = Ok<TValue, TError> | Error<TValue, TError>

local function divide(a: number, b: number): Result<number, string>
    if b == 0 then
        return err("Division by zero")
    else
        return ok(a / b)
    end
end

local x = divide(10, 2):unwrap()

Frityet avatar Nov 24 '25 18:11 Frityet

IIRC, that happens because each is BaseResult<...> application of type variables in the record declaration produces a new (distinct) type.

What happens if the divide function returns BaseResult?

hishamhm avatar Nov 24 '25 19:11 hishamhm

IIRC, that happens because each is BaseResult<...> application of type variables in the record declaration produces a new (distinct) type.

What happens if the divide function returns BaseResult?

that works, but then flow analysis works more annoyingly

local x = divide(10, 2)

if x is Ok<number> then
    print(x.value)
else
    print(x.err)
end

vs what id have to do

local x = divide(10, 2)

if x is Ok<number, string> then
    print(x.value)
elseif x is Error<number, string> then
    print(x.err)
end

its a kinda minor thing tbh

Frityet avatar Nov 24 '25 19:11 Frityet