Tables

The best way to understand Table is to think of it as 2 parts, A sorted Array part for indices that have the type of number, And a HashMap for everything else.

Note: Tables are the native data structures of Lua and while we offer a higher lever more verbose way of defining structures using the struct keyword we still need to fall back to a table from time to time. They are an integral part of interacting with the codes written in Lua as they will expect tables and might return one to the caller.

A table can be defined using curly brackets({}).

let table: Table = {
  "First Item",
  "Second Item",
  "Third Item",
  "Key1": "Value1",
  "Key2": "Value2",
  "Key3": "Value3", -- notice that we can have trailing commas in our tables
}

Note: In the example above we have used the type Table which is a wide type that can refer to any table or struct type, Alternativly we could specifically type our table using the Table Type Expression.

We can specify non string keys explicitly by surrounding them in brackets.

let table = {
  [2]: "Second Element",
  [1]: "First Element",
  [true]: "Element for true",
  [false]: "Element for false",
}

Retrieving Values

You can retrieve values from a table using the Index operator([]).

assert_eq(table[1], "First Item")
assert_eq(table[2], "Second Item")
assert_eq(table[3], "Third Item")
assert_eq(table["Key1"], "Value1")
assert_eq(table["Key2"], "Value2")
assert_eq(table["Key3"]: "Value3")

Iterators

Tables by default don’t implement the IntoIterator trait, It means that we can not iterate them similarly to other collections.

for value in table do -- Compile error, the table doesn't implement IntoIterator trait
  -- ...
end

Similar to Lua we should use ipairs to iterate over numeric indices.

for (index, value) in Table::ipairs(table) do
  print(value)
end

-- First
-- Second
-- Third

Note: As you can see items in the array part of our table have been printed in order.

The ipairs is a static method of the Table type which will return an iterator for the argument table. This iterator would walk through all numeric indices of the table with the same caveats as the ipairs function in the Lua interpreters. If we want to iterate over all key values in our table, We have to use pairs in place of the ipairs method.

for (key, value) in Table::pairs(table) do
  if typeof(key) == number then
    print('table[${key}] == "${value}"')
  else 
    print('table["${key}"] == "${value}"')
  end
end

-- table[1] == "First"
-- table[2] == "Second"
-- table[3] == "Third"
-- table["Key1"] == "Value1"
-- table["Key3"] == "value3"
-- table["Key2"] == "Value2"

Note: Notice that the items with indices other than number didn’t keep their initial ordering.

Table type expression

We can express the type of a table using a similar syntax to its initialization. For elements with a string key, it is as straightforward as annotating the types of each item like so.

let t: { a: string, b: number } = { a: "Hi", b: 42 }

For numeric keys we have to wrap the index inside of brackets([]).

let t: { [1]: string, [2]: number } = { "Hi", 42 }

We can also generalize the type definition of keys with a specific type.

Here is a table that would return a string for all of its number keys and return a boolean for string keys.

let t: { [number]: string, [string]: boolean } = { "Hi", a: true }

The boolean keys are also similar to numeric keys and can be defined by surrounding them in brackets.

let t: { [true]: string, [false]: string } = { [true] = "TRUE", [false]: "FALSE" }

We can define a method in our table type using the function type expression syntax.

let t: { sum: fn(number, number) -> number } = { sum: fn(a: number, b: number) => a + b }

Expressing the type of a table that contains keys that are not primitive is a little bit more complicated, Let’s say we have a table that would map some Table types to either a string or number value, What we can do to represent such table is to map all Table types to either string or number type and then at runtime check for the type of the returned value and act accordingly.

let t: { [Table]: string | number } = { [t1] = 42, [t2] = "Hi" }

let elem = t[key]
match typeof(elem) when
  "string" then handle_string_value(elem as string) end
  "number" then handle_string_value(elem as number) end
  else end
end

If in advance we know the type of our Table we can further narrow our type, Let’s say we only return string for the Table type and would return a number for anything implementing NumericVal now we can narrow down our result types down so there is no need for runtime checks.

let t: { [Table]: string, [impl NumericVal]: number } = { [t1] = 42, [t2] = "Hi" }

let e1: string = t[table_key]
let e2: number = t[trait_key]