Hi everyone!
In this small topic I want to write about some really simple and at the same time really useful typing constructions that people usually are not aware of.
Trick #1 — converting string to singleton type of this string
Usually, when we pass a string into generic function, Luau is thinking that T is a string and not a singleton type of string we passed:
Or for example we create a table that contains a string value under a key Key2. If we would take the type of this table and see the type of value under key Key2 we will again see that it has type string and not singleton of this string:
Or maybe you have heard of keyof<> type function that allows you to get union type of keys of table (like Key1 | Key2) and you wanted to make something like valueof<>? Unfortunately, we would again see string instead of "Hello" | "World":
But it turns out that if you take string into the function with this function signature, it will be read as a singleton of this string:
local function singleton<T>(value: T | ""): T
return value
end
Trick #2 — Take only types X, Y and return type the same as argument passed aka Janitor/Trove Problem
If you have ever tried any cleaning module such as Janitor, Trove or any other probably you have came across this problem:
function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any
function Janitor.Add<T>(self: Private, object: T, methodName: BooleanOrString?, index: any?) -> T,
- Trove allows you to pass only certain objects, but it returns
anywhich results you not being able to see methods of object you put inside of it unless you explicitly cast it back with::. - Janitor returns argument of the same type that you have passed, but it’s not doing any checks for the type of object you passed.
Let’s fix it!
type A = {
Something: string,
}
type B = {
Example: number,
}
type Allowed = A | B
local function example<T>(value: T & Allowed): T
return value
end
But we can improve this construction even further:
Trick #3 — Take only types X, Y and return different types for X passed and for Y passed
Let’s imagine we want to pass certain strings into our function and depending on what value it has we want to return different types. Probably you have ever tried to do this:
local example = {
Hello = {
Something = 5,
},
World = {
AnotherExample = "abc"
}
}
local function assist<T>(key: T): index<typeof(example), T>
return example[key]
end
Unfortunately, solver is not able to both typecheck the argument and also return right type depending on what we passed. We can solve it! There are actually 2 approaches to that:
Give me hints, I don’t need any typechecking of passed string:
local example = {
Hello = {
Something = 5,
},
World = {
AnotherExample = "abc"
}
}
local function assist<T>(key: T | keyof<typeof(example)>): index<typeof(example), T>
return example[key]
end
Now solver suggest both Hello and World:
And we have correct types returned:
Give me typechecking, I don’t need any hints for string I have to pass:
local function assist<T>(key: T & keyof<typeof(example)>): index<typeof(example), T>
return example[key]
end
That’s it for today.










