luau icon indicating copy to clipboard operation
luau copied to clipboard

Add a keyof(table) type

Open avion-sandwich-gout-television-asterion opened this issue 1 year ago • 10 comments

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.

MagmaBurnsV avatar Jun 26 '23 18:06 MagmaBurnsV

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

goldenstein64 avatar Jun 27 '23 05:06 goldenstein64

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.

alexmccord avatar Jun 27 '23 19:06 alexmccord

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.

ghost avatar Jun 28 '23 07:06 ghost

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 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).

strawbberrys avatar Jun 28 '23 18:06 strawbberrys

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)>

Gargafield avatar Jun 28 '23 21:06 Gargafield

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.

alexmccord avatar Jun 28 '23 22:06 alexmccord

Would be awesome to have this this, allows for much stronger and automatic types.

CompeyDev avatar Aug 15 '23 16:08 CompeyDev

I hope they are doing the same thing for valueof.

Sano-WasTaken avatar Sep 13 '24 02:09 Sano-WasTaken

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

aatxe avatar Sep 13 '24 04:09 aatxe