luau
luau copied to clipboard
Add a keyof(table) type
The problem
Currently, it's inconvenient to type check table content as I have to write the same thing twice, both in the type declaration and in the table.
For instance, a constant that use a bunch of strings as keys.
type PossibleOpinions = 'Cats'|'Dogs'|'Pizzas'|'Tacos'
local MY_OPINIONS = {
['Cats'] = 'I love cats!',
['Dogs'] = 'I like dogs! But I prefer cats!',
['Pizzas'] = 'When I think about pizzas I think about Peppino Spaghetti',
['Tacos'] = 'You mean that french dish where they put a bunch of stuff in a brick?'
}
function sayMyOpinion(about:PossibleOpinions)
print(MY_OPINIONS[about])
end
sayMyOpinion('Cats') -- ok
sayMyOpinion('Hexagons') -- not ok
The solution
Add a keyof type! It returns a union of all the keys of a table! It should be backward compatible like typeof and not so chaotic to integrate.
local MY_OPINIONS = {
['Cats'] = 'I love cats!',
['Dogs'] = 'I like dogs! But I prefer cats!',
['Pizzas'] = 'When I think about pizzas I think about Peppino Spaghetti',
['Tacos'] = 'You mean that french dish where they put a bunch of stuff in a brick?'
}
function sayMyOpinion(about:keyof(MY_OPINIONS))
print(MY_OPINIONS[about])
end
sayMyOpinion('Cats') -- ok
sayMyOpinion('Hexagons') -- not ok
This seems like it would be a really nice addition.
Currently, I'm trying to make a state wrapper that auto-types its elements for the methods. The problem is there is no current method of getting the types in table, so everything gets inferred as any:
local newState = State.new {
A = 1,
B = 2
}
newState:SetValue("A", 2) -- "A" is not inferred, and 2 cannot be typed
newState:GetValue("B") -- Returns 'any'
This proposal would solve half of the problem, as now I could type the Key params with keyof({} :: T), such as:
type State<T> = {
GetValue: (Key: keyof({} :: T)) -> any
}
However, this does not fix the problem with the return type or the second param for SetValue, so I think there would need to be another operator to fix this, like a valueof function or something of the likes. I'm not sure how this would all fit together, but in my mind, it would look something like:
type GetValue<T = {}, K = string> = (T: keyof({} :: T)) -> valueof({} :: T, "" :: K)
Obviously, fixing my problem is not trivial, but I don't think the addition of keyof would hurt any future attempts at solving it.
The way TypeScript solves @MagmaBurnsV's problem is using keyof, a type space indexing operator, and constrained generics, shown here. Implementors of this interface would have type-safe access and mutation of key-value pairs present in T.
interface State<T> {
GetValue<K extends keyof T>(k: K): T[K];
SetValue<K extends keyof T>(k: K, v: T[K]): void;
}
// note that keyof accepts a type, so OP would need to use 'keyof typeof MY_OPINIONS'
I've seen discussion about constrained generics somewhere in this repo, like #784, but nothing about implementing keyof (other than this post) or a type space indexing op.
I want to note this RFC outlining what kind of syntax the team might prefer for type functions and why. TL;DR use keyof<T> over keyof T or keyof(T).
- #589
- https://github.com/Roblox/luau/blob/master/rfcs/disallow-proposals-leading-to-ambiguity-in-grammar.md
For the record, there's a Index<T, K> type family that's going to solve the type indexing problem, so I wouldn't bother working on this design space.
As for the Keyof use case, there's a bunch of nuances to think about. Should Keyof also include all accessible keys via __index metamethod? What about if the table has indexers? The story gets blurry in a quick hurry.
I think keyof should also include all accessible keys via __index metamethod because it's very similar than directly putting keys in the table. I first thought of keyof(T) because of Luau having typeof(T) would keep the syntax similar, but I don't mind any alternative syntaxes.
For the record, there's a
Index<T, K>type family that's going to solve the type indexing problem, so I wouldn't bother working on this design space.As for the
Keyofuse case, there's a bunch of nuances to think about. ShouldKeyofalso include all accessible keys via__indexmetamethod? What about if the table has indexers? The story gets blurry in a quick hurry.
I don't believe it should include keys from __index because that isn't what you're asking for. If you wanted to do that you should have to directly specify it with keyof(t.__index).
I think it's better to not include __index, since (maybe in the future) a user could implement this behavior themself:
local MyClass = { Foo = "Bar" }
local OtherClass = setmetatable({ Hello = "World" }, { __index = MyClass })
type MyClass = typeof(MyClass)
type OtherClass = typeof(OtherClass)
type TableIndexed = Keyof<OtherClass> & Keyof<typeof(getmetatable({} :: OtherClass).__index)>
It just occurred to me that RawKeyof could be a thing that explicitly ignores __index, then Keyof includes anything in __index and all of the types in the __index chain.
Would be awesome to have this this, allows for much stronger and automatic types.
I hope they are doing the same thing for valueof.
valueof already exists in the new solver, it's called index and rawget: https://rfcs.luau-lang.org/index-type-operator.html and https://rfcs.luau-lang.org/rawget-type-operator.html