Luau mixed value types in table

  1. What do you want to achieve?
    I want to know how to use mixed types of data as values, specifically, with a table that follows the structure of {Type1,…} where Type1 is an already defined type, and … is a list of parameters used to interface with that type

  2. What is the issue?
    Luau seems to think that everything else in the table must be the first value type (HistoryActionContext):

  3. What solutions have you tried so far?
    I’ve tried all options here to define that table in a way that could allow mixed value types:
    image

Incase it helps, here’s what those other tables look like:

export type HistoryActionContext = {
	Name: string,
	UndoFunction: any, -- Function
	RedoContext: HistoryActionContext,
}
local UndoContext : {[string] : HistoryActionContext} = {}
local ActiveUndoState : HistoryAction = {}

I’ve also tried to look through the documentation? for Luau to try and understand more what I’m supposed to do here. I’m really new to Luau typing, and I’m primarily trying to write code that’s compliant so that other devs on the team I work for can have it turned on.

Would it be possible for you to use a dictionary that leads into a table value?

e.g.

ActiveUndoState[UndoContext.Paint] = {}
table.insert(ActiveUndoState[UndoContext.Paint], CurrentUID)
1 Like

No, it’s a list because it will have potentially tens of thousands of actions in it (all with the same first index). I could make the table itself a dictionary, but that feels like it would be wasting a lot of RAM/processing, in a project that is already going to need micro-optimization, and will be pushing limits in other areas of the code.

You don’t appear to have a way to index HistoryActionContext in your table. You can add it as such

type HistoryAction = {
    context: HistoryActionContext?, -- Type1 in your example
    [number]: any                   -- ... in your example
}

Alternatively because I’m not 100% sure on your question

type HistoryAction = {
    [number]: {
        context: HistoryActionContext?, -- Type1 in your example
        [number]: any                   -- ... in your example (numbered)
    }
}

You should then be able to insert an element as such

table.insert(ActiveUndoState, UndoContext.Paint.CurrentUID)   -- first option
table.insert(ActiveUndoState, {UndoContext.Paint.CurrentUID}) -- second option

I would recommend instead of allowing any to show up, define a union type of all the types you would like to be accepted as parameters. For example if you only wanted numbers and strings

[number]: number | string       -- ... in your example (numbered)

I would also recommend deciding if you need a HistoryActionContext always or if it’s only sometimes, if you realize you always want it to be there remove the ?. Note that if you do this the HistoryActionContext must be added on declaration of a table of the type.

context: HistoryActionContext,  -- Type1 in your example

Final note, a dictionary would not necessarily be a bad solution, just an outdated one, with Luau a table like this will behave almost exactly like a dictionary anyways. The reason to use a table like this over dictionaries is not performative (if anything the dictionary may be better optimized) but for readability. Tangent to that, you should seldom make concession to processing power, if you actually find your program slowing down your computer there are likely structural problems that need to be addressed, not list optimization problems.

1 Like

Did you mean to write:

type HistoryAction = {
  [number]: HistoryActionContext | number
}

You probably wrote:

type HistoryAction = {
    [number] = {
        [number]: HistoryActionContext | number
    }
}

It’s a bit unclear what you’re trying to do.

1 Like

ActiveUndoState itself is an un-finalized version of HistoryAction. It’s an ordered list of smaller actions, that are defined by a HistoryActionContext and a number. The structure is intentional, and the code currently works, although because it sees index 1 as HistoryActionContext, it thinks the entire table should have HistoryActionContext’s in it, when I only want it to be index 1

In my example, I tried a union of [number]: HistoryActionContext | any, and still had the same blue-lining complaint that it was expecting a HistoryActionContext for index 2. The reason I wanted to use any (and not a more strictly defined type), is that indices 2,3,4,etc. are all parameters for one of the functions stored in HistoryActionContext, so the length of that table is unknown, as well as the types of the values. It does not just have numbers as the values, it could be strings, other objects, functions, Vector3s, etc. The first index is always HistoryActionContext though.

Since you do seem to know the most luau of the replies though, are mixed value tables just unsupported? Surely there would be a way to have the index be a number, and the value to be mixed value types?

I’m pretty aware of the evils of premature optimization, though thanks for the caution. This project is going to be an element that will be added to other games - It’s already intensive enough that portions of the project use considerable percentages of CPU, even with thought being put into structure and storage of information. It’s just incredibly math intensive, and the amount of data to process is easily hundreds of thousands of items. To add to that, this project is being placed as a component into a game that is already struggling to have consistent performance (though is not optimized yet- once this project is ready, I’ll be devoting effort to making the game ready to handle the extra CPU load). Even just the index cost of an extra string, and the cost of later splitting that table with the string in it is worth considering in this case.

May you provide a visual table example (so how the table would look in game will the contents). It’s current difficult to decipher what kind of table you’re trying to make.

So the way to do this in Typescript (what Luau is based off of) requires using Tuples in the type, however Luau doesn’t support this as far as I know. While you can use a mixed value table, as per my example, the HistoryContextAction will always need to be indexed differently than the rest of the parameters, they cannot all be in a single numerically indexed list.

A roblox-ts example of the code you want (not doable in Luau)

type HistoryActionContext = {
  Name: string;
  UndoFunction: any;
  RedoContext: HistoryActionContext | any;
};
type HistoryActionElement = [HistoryActionContext?, ...Array<{}>];

type HistoryAction = Array<HistoryActionElement>;
const ActiveUndoState: HistoryAction = new Array<HistoryActionElement>();

const TestContext: HistoryActionContext = {
  Name: "Hi",
  UndoFunction: 5,
  RedoContext: null,
};

ActiveUndoState.push([TestContext, 5, "Potato", new Vector3(0, 0, 0)]);
ActiveUndoState.push([undefined, 5]);
1 Like

The other guy said it’s not possible, so you don’t need to bother with understanding, but if you have extra input, an example table might look like this:

local Context1 = HistoryActionContext.new()
local Context2 = HistoryActionContext.new()

local HistoryAction = {
	{Context1,number},
	{Context1,number},
	{Context2,string,number,string},
	{Context1,number},
	{Context2,string,number,string},
	{Context2,string,number,string},
	{Context1,number},
}

Can’t you just do:

type HistoryAction = {
    [number]: {
        [number]: Context | string | number,
    }
} 

If you want to separate the tables that have numbers in then you can switch to:

type HistoryAction = {
    [number]: {
        [number]: Context | string | number,
    } | {
       [number]: Context | string, 
    }
} 
1 Like

No, once I add the first Context to the table, it’s expecting for the entire table to be full of Context. An or was the first thing I tried. It won’t even work with an any.

I still do not understand your issue. Can you create a table with all the types listed in it (a table including all the types, especially the conflicting ones)? If you were to typecheck the visualized table you provided with the type I suggested it wouldn’t have any warnings.

1 Like

I just made a quick sample script, and it did throw warnings:

Script text if you wanna try it out:

--!strict

type Context = {
	[string]: string
}

type HistoryAction = {
	[number]: {
		[number]: Context | string | number,
	} | {
		[number]: Context | string, 
	}
} 


local DefaultContext: Context = {Purpose = "Hi"}
local Table: HistoryAction = {[1] = {DefaultContext,"123",123}}
1 Like

I thought you had 2 different types. That’s why I suggested:


If you just remove the second type it works perfectly:

image

--!strict

type Context = {
	[string]: string
}

type HistoryAction = {
	[number]: {
		[number]: Context | string | number,
	},
    --> removed other type because it was uneeded
} 


local DefaultContext: Context = {Purpose = "Hi"}
local Table: HistoryAction = {[1] = {DefaultContext,"123",123}}
1 Like

It will have multiple options in the future- an unknown number of options, actually, since other scripts interacting with this module can create the context that they use. So in your example above, is there a way to write it where it could have either 1, 2, or X different number of options, without warning?

1 Like

I’m not quite understanding your problem. May you elaborate? Preferably, by showing examples.