Common Dictionary Type Definition "Cannot add indexer to table" Warning Solution

I thought I’d make this it’s own post, since I haven’t been able to find any recent posts pertaining to it, and I don’t want to resurrect dead threads. (doesn’t that mean it’s not a useful post..?)

I’ve personally encountered this a few times, and I can imagine it may trip someone up who doesn’t know how to fix it, so I wanted to share my solution.

I’m sorry that I cannot provide a very technical explanation of the underlying mechanism, but it should prove adequate.

But please note if you’re iterating through a dictionary with mixed types, you would have to explicitly check types (using typeof()), as the iterator doesn’t know which of the applicable types it is, which personally I’d avoid doing in every circumstance.
(Note that this is NOT limited to only a defined dict, it can of course be done with defined types, too, simply do not use typeof() in that case)

-- Automatic example:
local AutoDict = {
    VariableA = 200,
    VariableB = 250
}
AutoDict = AutoDict :: {[keyof<typeof(AutoDict)>]: number} -- Now iterable, specifically a number
AutoDict["VariableA"] -- Autocompletes, doesn't get mad!
AutoDict.VariableA -- Still autocompletes! This is the standard for the following example.

But this can be dangerous, because you won’t be told if the type ‘number’ is not applicable to the dictionary. So, you can create a helper to ensure it’s accurate:

type HelperDictItem = number
local function NewDictItem(a: HelperDictItem): HelperDictItem
    return a
end

local HelperDict = {
    VariableA = NewDictItem(200),
    VariableB = NewDictItem(250)
}
HelperDict = HelperDict :: {[keyof<typeof(HelperDict)>]: HelperDictItem} -- We successfully moved the type declaration to one the dict must follow.

This is more helpful when your values are unions, but you should avoid distinct unions as types in dicts.

An example of a dict that is hard to work with would be:

type ExampleKeys = "VariableA" | "VariableB" -- At it's roots, keyof<> really just creates a union similar to this, this is just manual not automatic.
type ExampleDict = {[ExampleKeys]: boolean | number | string}

It’s filled with different types (boolean and number and string), which makes it hard to work with especially when iterating.

But a clean use of unions for a dictionary would be something like:

type ExampleKeys = "VariableA" | "VariableB"
type ExampleDict = {[ExampleKeys]: "ValueA" | "ValueB" | "ValueC"}

Please know however that there are many pitfalls when type checking and you may encounter difficult situations because of your own design, so be careful. I suggest never getting yourself in the latter example’s situation, but if you must you can whilst still maintaining types to the best of the solver’s ability.

I hope that you learned something from this, that you don’t mind the bizarre inhuman names, and that this adequately explains this common scenario briefly and accurately (I haven’t slept)

If I missed something, or I’m actually scripting with terrible habits, please tell me, people!
If you have anything helpful to add that is similar to this, please share for everyone.

I wish you all the best, especially if just learning type-checking, it can be pretty tricky sometimes.

I haven’t made many posts, so I’m inviting any feedback on the post itself, thank you for reading!

A better way to go about this is to write your types first, then have your implementation follow the schema, rather than have your types follow your implementation.

This helps with the design process of your program, and helps you actually catch bugs when using strict-typing, without the use of such workarounds.

For example, to solve your problem:

--!strict

-- Declare your types first, then implement them.
type FooDictionary = {
	Bar: boolean,
	Baz: string,
	[keyof<FooDictionary>]: boolean | string, -- You can now safely iterate over tables of this type, although it is certainly still not the cleanest for developer experience, expect this to improve as the type engine continues to evolve.
}

-- You will now get type errors for incorrect types in strict-mode.
local foo: FooDictionary = {
	Bar = "", -- TypeError: Expected this to be 'boolean', but got 'string'
	Baz = "Hello world!"
}
2 Likes

This is a very elegant solution, it pointed out flaws in my understanding of iterators.

Your solution is far more thorough and robust when types of the members vary, both when instantiated and when referenced.
I wonder if you would do anything different if all members in the dictionary are of the same type? (aside from changing the types)

Thank you for your insight, and for taking the time to write such an intuitive reply.

Not quire sure what you mean by this, but what I’d do with the types depends on what the dictionary is storing and whether it would be useful to know the exact properties of it, or just the indexer.
For example, a dynamically generated look-up table would not benefit from knowing its exact properties, whilst a configuration table might.

2 Likes