lua-language-server icon indicating copy to clipboard operation
lua-language-server copied to clipboard

Support discriminated unions / type narrowing

Open grapereader opened this issue 4 years ago • 4 comments

In TypeScript this is supported by "narrowing"

Consider this example:

---@alias ChatAction { type: '"chat"', targetName: string, msg: string }
---@alias NotifyAction { type: '"notify"', msg: string, colour: number }
---@alias ActionType ChatAction | NotifyAction

---@param action ActionType
local function example(action)
    -- action is any action (properties 'type', 'msg')
    if (action.type == 'chat') then
        -- action is ChatAction (properties 'type', 'msg', 'targetName')
    end
    if (action.type == 'notify') then
        -- action is NotifyAction (properties 'type', 'msg', 'colour')
    end
end

For a union type such as 'ActionType' in the example, the only defined properties should be those common to all types. After narrowing (through some sort of type check, in this case a type field) the additional properties in the narrowed subset are also available.

grapereader avatar Oct 03 '21 00:10 grapereader

:eyes:

ghost avatar Jan 05 '22 12:01 ghost

I was looking for a TypeScript narrowing solution for Lua as well. I have to always add ---@cast v string after each call to a custom function that guarantees a narrowed-down type. call. For example:

---@return string|number|boolean|nil
local function ExecuteAction()
  -- do stuff
end

---@param v any
local function AssertString(v)
   assert(type(v) == "string", "Not a string!")
end

local actionName = ExecuteAction()
AssertString(actionName)
---@cast actionName string

local result = actionName .. " executed!"

Whereas I would prefer to add some annotation to the AssertString method to tell LuaLS that I can confirm it is a string so treat it as a string going forward.

I would personally prefer to see annotation that simulates the syntax of TypeScript's narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

In TypeScript:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

So, for LuaLS annotation, this could look like:

---@param pet table
---@return pet is Fish
local function IsFish(pet)
   return pet.swim ~= nil
end

Where returning an is narrowing expression also means it returns a boolean, but if the boolean is true then the argument passed to the parameter is of that type.

For my assert example, I think this may be an additional feature on top of the type predicate narrowing example because no boolean value is being returned. Also, the code using the function shouldn't need to use if AssertString(val) then ... end.

So maybe that needs to use some sort of @asserts value is T annotation:

---@asserts value is string
local function AssertString(value)
  assert(type(value) == "string", "Not a string!")
end

Similar to how TypeScript Assertion Functions work:

type Data = { foo: string };

function assertData(d: Data | null): asserts d is Data {
    if (d == null)
        throw new Error("Invalid data");
}
// Use
declare var bar: Data | null;
bar.foo // error as expected
assertData(bar)
bar.foo // inferred to be Data

EDIT: I've created a new follow-up feature request for Assertion Functions that expand on narrowing with type predicates: https://github.com/LuaLS/lua-language-server/issues/2032

Mayron avatar Mar 26 '23 10:03 Mayron

That would be awesome to have this feature, yes!
And it would be nice if it also worked like this:

function on_message(self, message_id, message)
 if message_id == message_type.ATTACK then
        -- autocomplete know the message has `speed` and `health`
        print("Speed: " .. message.speed)
        print("Health: " .. message.health)
    elseif message_id == message_type.JUMP then
        -- autocomplete know the message has `height` and `time`
        print("Height: " .. message.height)
        print("Time: " .. message.time)
    end
end

AGulev avatar May 27 '24 08:05 AGulev

This is partially implemented in https://github.com/LuaLS/lua-language-server/pull/2864 which supports narrowing for literal fields on classes.

lewis6991 avatar Sep 23 '24 09:09 lewis6991