Skip to content

Support discriminated unions / type narrowing #704

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

Open
grapereader opened this issue Oct 3, 2021 · 4 comments
Open

Support discriminated unions / type narrowing #704

grapereader opened this issue Oct 3, 2021 · 4 comments
Labels
enhancement New feature or request

Comments

@grapereader
Copy link

grapereader commented Oct 3, 2021

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.

@sumneko sumneko added the enhancement New feature or request label Oct 8, 2021
@ghost
Copy link

ghost commented Jan 5, 2022

👀

@Mayron
Copy link

Mayron commented Mar 26, 2023

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:
#2032

@AGulev
Copy link

AGulev commented May 27, 2024

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

@lewis6991
Copy link
Contributor

This is partially implemented in #2864 which supports narrowing for literal fields on classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants