Type Annotations! A guide to writing Luau code that is actually good

Maybe I missed it, but I didn’t see any explanation of typecasting (::)?

1 Like

Nope, I did forget to cover it.
I’ve added a new typecasting section near the bottom :sunglasses:

2 Likes

Sometimes the never type is used for annotating unintended members of a table to be indexed.

For example, __index in a metatable.

1 Like

Definitely an interesting way to go about that. For me I just prefix stuff intended to be private with an underscore.

2 Likes

I have a question that after reading the guide I still didn’t get the answer and I’m starting to get scared it’s not possible…
Let’s say I have this type:

F: (Instance) -> boolean

But I want to let F accept any subclass of Instance as well, can this be possible without doing an :IsA check?
for example:

F = function(frame: Frame)
...
end

What the type Instance really means here is any class that derives from Instance. And, as you know it, all classes from derive from it, meaning any class in the engine is a valid parameter to the function.

That’s what I thought too but it wont let me do it.
I tried:

F = function(frame: Frame)
frame.Visible = false
end

and it made red everywhere

While Frame might inherit from Instance, Instance does not inherit from Frame, and we cannot assume that to be the case.

-- invalid code

local function hide(frame: Frame)
    Frame.Visible = false
end

for _, child: Instance in ipairs(game:GetChildren()) do
    hide(child)
end

So here’s the thing: if you know for sure that the object being passed to this function is of the correct type, you can help the type-checker by type-casting the argument:

local frame: Instance = Instance.new("Frame")

hide(frame :: Frame)
-- This is just an example. I know marking `frame` as `Instance` is redundant.

I imagine in your case that when you call F, the parameter has not been given an appropriate type yet. You could use type casting, as I said before, or even better: strict typing.

It doesn’t work because you cannot plug in a function that takes a different type from Instance into a function that expects to consume an Instance.

You can just do a quick check of IsA to refine the type into Frame.

Something I wanted to add (which I have no idea is working as intended) is that typecasting will trim an ellipsis to its first value.

local function Vararg(...: number)
	return ... :: number
end

print(
	Vararg(1, 2, 3) -- returns just 1
)

It appears to evaluate faster than select, so I’ve been using it to discard extra values in functions i.e. string.gsub(A, B) :: string. If someone could explain why it happens though that would be great.

What do I do if I want to put an unknown (…) amount of functions as a type? Like a weapon with multiple abilities for example.

export type WeaponInfo = {
	Class: string,
	Damage: number,
	Ammo: number,
	Auto: boolean,
	Abilities: (...any) -> () 
-- i want it to be possible for abilities to be multiple functions, maybe a table of them or something
}

Make it a dictionary with string as keys and functions as values?

Something like this? (keep in mind im stupid)

Abilities: {(...any) -> ()}?

That or

Abilities: {
    [string]: (...any) -> ()
}

Depends on how you want to append the functions to the abilities table. Through table.insert, or Abilities.SomeAbility = func

1 Like

I know this topic hasn’t been checked for a while, but I can’t seem to be able to solve this issue:

type a = {
	epic: string?,
	amazing: number?
}

type b = a & {
	super: string?
}

local tbl: b = {}
local key = "epic"

tbl[key] = "" -- Expected type table, got 'a & { super: string? }' instead.

When defining a custom type that includes another type, and then indexing that table with the custom type with a variable, the typechecker displays this warning.
It doesn’t occur when I do it like tbl["epic"] = "" though.

The problem is that key is inferred to have a type of string, since by default Luau assumes you’ll want strings to be dynamic rather than string constants. At a static analysis level, this means it doesn’t know what string is indexing the sealed table type and assumes it’s unsafe.

Even if you define it explicitly as a constant though, I’m not sure if it’ll work. You should always just write to those fields explicitly if you’re using a dictionary type without an indexer.

How can I define it explicitly as a constant? Like key: string ?

Okay I’ll be honest, looking closer at the issue now I don’t really know why you are getting that warning. It seems like Luau doesn’t like variable access on sealed union tables. Must be some bug.

You can get Luau to cooperate if you upcast the type back to a like this:

(tbl :: a)[key] = ""

In general though, dynamic access to tables isn’t an advised practice within Luau’s typechecking. You can do it (and it’s not unreasonable to do so), but you have to do it carefully with your own runtime error catching code, outside the bounds of Luau’s typechecker (via any casting when appropriate)

Thank you! That really works. And I think it might really be a bug. Because when I also do something like this:

type a = {
	epic: {[string]: any}?
}
		
local b: a = {}
		
for key, value in b do
	if typeof(value) ~= "table" then continue end
	-- value should now be the type: table
	
	value["randomKey"] = "hi" -- Expected type table, got 'unknown' instead.
end