Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Late definitions" - define members AFTER asserting type #1099

Open
PhoenixWhitefire opened this issue Nov 11, 2023 · 3 comments
Open

"Late definitions" - define members AFTER asserting type #1099

PhoenixWhitefire opened this issue Nov 11, 2023 · 3 comments
Labels
enhancement New feature or request fixed by new solver This issue is confirmed to be fixed in the new solver. type states

Comments

@PhoenixWhitefire
Copy link

PhoenixWhitefire commented Nov 11, 2023

I'm not sure if this has been suggested already, but I've run into this issue with typechecking where I say that something has a type, but it gives me a warning because the variable doesn't have all it's members upon declaration:

type MyFunType = {
    Something: () -> ()
}

-- It's just an empty table right *now*, I just define it's members *later*.
local ObjectThatDoesSomething: MyFunType = {}
-- The above gives warning: "Table type 'ObjectThatDoesSomething' not compatible with type 'MyFunType'
-- "because the former is missing field 'Something'"

function ObjectThatDoesSomething.Something()
    while true do
    end
end

In the above example, even though I have indeed defined ObjectThatDoesSomething.Something, it still acts like I haven't because when I tell the typechecker ObjectThatDoesSomething is of the type MyFunType, it's just an empty table.

Ideally, it would be able to see ahead in the script to check if I've defined the members afterwards. A workaround would be to define all the members directly in the table, but I don't really like that stylistically:

type MyFunType = {
    Something: () -> ()
}

-- No warnings.
local ObjectThatDoesSomething: MyFunType = {
    Something = function()
        while true do
        end
    end,
}
@PhoenixWhitefire PhoenixWhitefire added the enhancement New feature or request label Nov 11, 2023
@JohnnyMorganz
Copy link
Contributor

You can get the behaviour you want using the type assertion operator.
Unsure if that is the recommended way to do this though

type MyFunType = {
    Something: () -> ()
}

local ObjectThatDoesSomething = {} :: MyFunType

function ObjectThatDoesSomething.Something()
    while true do
    end
end

@PhoenixWhitefire
Copy link
Author

PhoenixWhitefire commented Nov 12, 2023

@JohnnyMorganz the issue with that is that I'm "asserting" what the type is and sidestepping the typechecker entirely. Ideally it should be smart enough to figure out that there's no warning to be had instead of me having to force it off, because otherwise there might be a situation where I do forget to define a member because there wasn't a warning.

@AmaranthineCodices
Copy link
Contributor

The way that Luau treats type annotations makes this not workable. A type annotation on a local is a promise that at all points in the program, that local fulfills that type annotation's constraints. This is why Luau is reporting a type error on the initial assignment: because at that point in the program, the local doesn't fulfill the constraints of its annotation. Consider something like this:

type T = { x: string }
local t: T = {} -- no error
if cond() then -- Luau doesn't know if this branch is taken
    print(t.x) -- oh no
end

t.x = "bar"

In order for Luau to prove that this kind of initialization is safe generally, we need to do more robust control flow analysis than we do today, even in the new type solver. We eventually need to do that control flow analysis for other reasons, but it's not something that's likely to come soon. Even when it does arrive, I'm not sure we'll relax this restriction, because of how Luau considers type annotations to be binding promises that hold at all points in the program.

For this kind of initialization, I'd suggest something like this:

type MyFunType = {
    Something: () -> ()
}

local function Something()
    while true do
    end
end

local ObjectThatDoesSomething: MyFunType = {
    Something = Something,
}

Another option would be to construct the table in another local, and then assign to the annotated local once the table is fully constructed:

type MyFunType = {
	Something: () -> ()
}

local obj = {}

function obj.Something()
	while true do
	end
end

local Object: MyFunType = obj

@alexmccord alexmccord added type states fixed by new solver This issue is confirmed to be fixed in the new solver. labels Dec 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request fixed by new solver This issue is confirmed to be fixed in the new solver. type states
Development

No branches or pull requests

4 participants