Luau Recap: August 2020

Wondering about the following type definition:

type Foo = {[nil]: string}

Table keys are not allowed to be nil, so this definition can only be satisfied by an empty table, which may or may not be useful. It also allows only nil to be indexed:

local foo: Foo = {}
print(foo[nil]) --> nil

Additionally, an optional type can be used as a key:

type Foo = {[string?]: string}

Obviously, you still can’t assign to the nil key, but you can still get from it, which may or may not be useful:

local foo: Foo = {bar="baz"}
print(foo["bar"]) --> baz
print(foo[nil]) --> nil

My question is, should nil type keys be disallowed, or are these behaviors useful enough to keep them as-is? I could see the second definition being useful where the user wants to index the table with an optional type. But the first definition, where the key is just nil, seems rather quirky.

2 Likes

The type checker seems to ignore the values of a constructed table when the table value type is defined as nil:

type Foo = {[string]: nil}
local foo: Foo = {["foo"] = 42} -- No warning
foo.bar = 42 --> Type mismatch nil and number

Additionally, The following causes a crash, but only when !strict is enabled:

--!strict
type Foo = {[string]: nil, String: nil}
local foo: Foo = {["foo"] = 42}
2 Likes

An implicit assertion is missed. Consider the following code:

type Foobar = {Foobar: string}

local function Func(v: Foobar|string): string
	if typeof(v) == "string" then
		return v
	end
	return v.Foobar --> Type Foobar | string does not have key 'Foobar'
end

print(Func("string"))
print(Func({Foobar="foobar"}))

The conditional correctly asserts v as string, as expected. Beyond the conditional branch, the only possible type v could be is Foobar. However, this is not detected.

2 Likes

Yeah we didn’t think that this was important enough to be invalid syntax, but we should make a lint rule for this and string | number? which reads weirdly. You’d silence this warning by writing (string | number)? instead.


Lua 5.4 does the same thing! local n<const>=1 is invalid syntax there.

We should nonetheless make this work.

2 Likes

I have found another issue with format string analysis:
image
[%%] is a valid pattern, but presumably it thinks the second % escapes the ]?

1 Like

I’ve been using typed Luau a bunch in the last few days to rewrite an older project (you’ve probably seen me post a few bug reports and feature requests in the last several days) and one thing I will say stands out is how hard it is to work with without an equivalent to as.

The bulk of the work has thus far been on modernizing an API tool so that I can easily access information about classes and their members, and part of that involves writing type aliases for the API dump JSON. That in of itself was easy enough, but the API dump has a few optional fields in places.

The finished type alias for something like a class member ended up looking something like this:

type Member = {
  Name: string,
  MemberType: string,
  Security: string | { Read: string, Write: string },
  Tags: Array<string>?
}

Note: this isn’t the entire alias as it’s quite large and not important to this post

The ones to focus on are Security and Tags.

In order to work with Tags, I had to do a truthy check on it, which while fine, has an annoying bug that I hope is fixed soon (:eyes:).

Security is a different beast though. At a glance, it seems easy to handle:

if type(Security) == "string" then
  -- Security will be a string here!
else
  -- Security will be a table!
end

This turned out to not work though, which turned out to be a major bummer. I’m not sure if this is a bug or not – if it is, I’ll file a bug report as soon as I’m told.

Refining the type from there is… Outright impossible. To clarify, in a strict environment, to my knowledge it is currently impossible to refine a type such that it will only be a table. Something like typeof(Security) == "table" doesn’t work, and you can’t directly index Security to check if it’s a table because then the script analyzer will raise an error about Read not being a valid member of string.

I ended up solving this problem by just passing the table through to a non-strict module which solved the problem with a simple if-statement, but that’s not ideal! We need some way to tell the type system that something is a particular type, because it’s currently not smart enough to know these things (and to be honest it’s a bit much to expect it to ever be perfect).

I once thought that a simple type assertion operator (! in TypeScript) would be enough but I’m no longer of that opinion. I realize there’s no easy way to have as be a valid keyword because you can call functions like as{} but there has to be a solution here.

While talking with a few people a while back, we came up with a few potential solutions, ranging from something as simple as requiring expressions be wrapped in parentheses (e.g. (foo as {}) vs foo as {}) or introducing a symbol (@ might be good but there be dragons). Just… please add some way to do it.

3 Likes

Thanks - yeah the new scanner for sets didn’t carefully handle %] when the first % was escaped. Will be fixed next week.

I don’t think having optional types in table indexers’ key is ever useful enough to warrant some use cases like that. We should warn on it, thanks for bringing this up.


Definitely a bug. For what it’s worth, the expression {["foo"] = 42} actually produces the type {foo: number} in strict mode, {foo: any} in nonstrict. Not a table with an indexer whose key is string, producing a number.

This is fixed in 445!


Yeah, this is because Luau doesn’t do any real flow analysis right now. It’s on the roadmap too.

3 Likes

Lovin’ LuaU!

For those who want, here are some useful data types:

type array<t> = {[number]:t}
type dictionary<t> = {[string]:t}

-- Examples:
local myTableA:array<string> = {"a","b","c"}
local myTableB:dictionary<any> = require(module)

I don’t know if here is the correct place to make a Luau feature request.
Anyway, it would be great if I could create a sub-operation inside an if, like C++.
Ex:

if (i = 1+1) == 2 then -- puts the result of 1+1 inside `i` and compare if i == 2

In Lua assignments are statements, not expressions, and this can cause unintended behavior like someone forgetting to add an extra = when they intended to compare i to 1 + 1 but not assign i to 1 + 1 and check if 2 is truthy.

That is not happening.

The fact that you can do that in C is widely regarded as a mistake, as it creates countless bugs where developers accidentally type if (a = b) instead of the if (a == b) they actually meant.

Any plans for string literal types?

Y’all do type checking on the arguments for Instance.new, for example. It would be cool if we could also have access to that ability. I suppose it is less useful in Luau than in TypeScript, though.

3 Likes

It would be great if I could to create an “alias” variable to change a value for a long dictionary sentence, ie, to change a value by reference, as currently can be used in c or c++ (C++ References).

For example, currently if I have to change a value of one element of this complex dictionary…

CellAux[Key][4][Materials[Material].Output].LastMaterial = Material

… and in every point of the program I need to change it, I have to repeat this long sentence every time.
But if I could do something like c++, like:

local &LastMat = CellAux[Key][4][Materials[Material].Output].LastMaterial
LastMat = Material -- will actually change the value of "CellAux[Key][4][Materials[Material].Output].LastMaterial"

This would improve a lot the code readability.

That could be nice, but there are two issues. For one, your game/code structure could and should probably be changed to avoid that kind of messy assignment. For two, functions exist. You can just define a function to do that for you. On another note, this would be great for cases where neither of those apply.

You can already get most of the way there and get almost the same readability though:

local LastTb = CellAux[Key][4][Materials[Material].Output]
LastTb.Material = foobar(LastTb.Material)
1 Like

BrickColor.Random is missing:

image

Try lowercase r (BrickColor.random()). Roblox changed it to be more consistent with the rest of the API where constructors for other types are in camelCase instead of PascalCase.

table.create is typed to require two arguments, but the documentation says it’s optional.

table table.create ( number count, Variant value )
Creates a table with the array portion allocated to the given number of elements, optionally filled with the given value .

image

1 Like

Would love a feature similar to // @ts-expect-error or // @ts-ignore for type errors that we know about and expect/don’t want to deal with.

1 Like