Migrating the Love2D Pong game to Teal, a statically typed Lua
- YT :: https://www.youtube.com/watch?v=8lwogk6xeM0
- Original title :: Statically Typed Lua
Tsoding's main complaint about Lua after the previous Pong stream was dynamic typing, which makes refactoring and extending code painful. Chat suggested Teal, a statically typed Lua dialect that compiles to plain Lua, roughly what TypeScript is to JavaScript. He has never used it before. He downloads the prebuilt binary, migrates his 269-line Love2D Pong game to Teal by writing his own minimal Love2D type declarations, and then uses the type checker to add a pause feature with a "PAUSED" label drawn in the game's segment-style pixel font. His verdict is that it is the smoothest language adoption he has seen, and that Teal is now his default for any Lua work, the same way TypeScript is for JavaScript. His one serious complaint is that tl gen emits Lua without type checking.
Getting started
- Docs suggest installing via LuaRocks. He refuses ("npm-style" package manager that even had a security incident in September) and grabs the prebuilt Linux tarball instead.
tlis an almost static ELF binary, and Teal is written in Teal. - Running
tlwith no arguments only says "a command is required", with no list of commands and no hint. He compares it to his own issue trackertatr, which prints usage when run bare.tl --helpworks. - Commands:
check(type check),gen(emit.lua),run(run a.tlscript with a built-in Lua interpreter, so no external Lua is needed),warnings,types, andcompletion(bash completion; he wonders why that is a subcommand). tl genon an emptymain.tloverwrote his realmain.luawith a single newline. Version control saved the game.
Language basics he ran into
- Functions need an explicit
localorglobal. There is no gradual typing: untyped parameters are errors, and the tool warns about unused functions and variables. - Generated Lua simply has the type annotations stripped.
- Arrays are written
{number}and 2D arrays{{number}}, mirroring table literal syntax. He likes this. - Maps are
{K:V}. Records model Lua tables with fields. Methods declared with:need the self type as an explicit first argument in the function type. - There is a separate
integersubtype ofnumber. Literals such as0infer asinteger, so variables that later hold floats must be annotatednumber. Scores, on the other hand, should be integers. He finds it "too strong now", but prefers that to too weak. - There are no TypeScript-style string literal types (
"fill" | "line"), which he calls the coolest part of TypeScript. Teal hasenuminstead, and enums cannot be declared inside a record; they must be top level (local enum DrawMode "fill" "line" end). - Values are nullable by default.
Joystick | nildid not narrow after a nil check, so method calls failed. Dropping the union fixed it. - No
+=. He thinks a transpiler should have added it. - The error count is printed at both the top and the bottom of the output, which he likes.
Writing Love2D declarations by hand
- Third-party types go in a
.d.tlfile loaded withrequire. Full generated Love2D typings exist on GitHub, but he only uses a small part of the API, so he writes his ownlove.d.tl:graphics(rectangle,clear,line,setLineWidth,getWidth),window(setMode,setTitle),audio(newSource,play,setVolume),joystick(getJoysticksreturning{Joystick}),keyboard.isDown, and enumsDrawMode,SourceType,KeyConstantandGamepadButtoncontaining only the values the game uses. - He got stuck on defining the
love.load,love.updateandlove.drawcallbacks inmain.tl: Teal refuses to add undeclared functions to a record outside the scope where it was declared. The fix (found in someone's Love2D Teal repo) is to declare the callback signatures in the.d.tlrecord first. Not obvious, and the docs have no example. - Records and enums referenced across files must be
global, notlocal. - He installed the six-year-old
teal-modefor Emacs from GitHub. It works, though it doesn't highlight =enum=/=type= and indents oddly. - The error count dropped steadily from about 70 to zero. The generated Lua runs unchanged. Comments are lost in the output.
- Build process: commit only
.tlfiles, gitignore.lua, runtl gen main.tl && love .. Users already have to install Love2D, so installing Teal too is fine ("we don't commit a C compiler to C projects").
Adding pause, guided by the type checker
- A first attempt used
love.keyboard.isDown("space")even thoughspacewas not inKeyConstant, and it still compiled. That's becausetl gendoes not type check at all; onlycheckandrundo. He finds this split absurd: no C compiler lets you skip checking and generate code anyway. isDownis the wrong tool for a toggle. He switches to thelove.keypressed(key, scancode, isrepeat)callback, declares it inlove.d.tl, and uses_for unused parameters.- Renaming
render_digittorender_glyphanddigit_sizetoglyph_sizeby "just following compilation errors" is the moment that sells him: "this is why static typing won". He grants that dynamic typing suits some problems but not a whole language. - He adds letter glyphs P, A, U, S, E, D to the segment font. Then he makes the font a map keyed by a
FontGlyphsenum, so text is a{FontGlyphs}array instead of a string. Rendering a letter that has no glyph becomes a compile error. That is fine here because all text is static. text_widthuses#text. The "PAUSED" label is centred by subtracting half its width and height (5 glyph rows), and the checker caught a typo along the way.- Implementing pause felt stress-free "because I know it's all type checked".