Skip to content
Development
Skill

/strict-typing-luau

Convert a Nevermore Luau file from --!nonstrict (or untyped/--!nocheck) to --!strict, adding the project's explicit type annotations and fixing every type error the checker reports. Use this whenever the user asks to "strictly type", "add types to", "make strict",

From plugin
nevermoreengine
6281 skill

Claude Code

$ npx -y skills add Quenty/NevermoreEngine --skill strict-typing-luau --agent claude-code

SKILL.md

strict-typing-luau.SKILL.md
name: strict-typing-luau
description: Convert a Nevermore Luau file from --!nonstrict (or untyped/--!nocheck) to --!strict, adding the project's explicit type annotations and fixing every type error the checker reports. Use this whenever the user asks to "strictly type", "add types to", "make strict", "type-annotate", "convert to --!strict", or clean up the typing on a .lua/.luau file in this repo — including when they just select a file with a `--!nonstrict` header and say "type this" or point you at a legacy module. Also use it when a strict file is throwing luau-lsp type errors and the user wants them resolved following Nevermore conventions.

Strictly typing Luau files

Convert a file to `--!strict` and make it pass the type checker cleanly. This is mostly **mechanical pattern application** — move fast and correctly. The checker is your fast feedback loop; lean on it instead of guessing.

Triage first — match effort to the file

  • **Plain util / data module** (`local X = {}` of functions, or a `return {...}` table; no

`setmetatable`): flip the header, give every function typed params/returns, run single-file analyze. Usually no `export type` block needed.

  • **Class** (`setmetatable({}, ...)` + constructor + methods): the real work — needs the

`export type` block. Follow the patterns below and `references/conventions.md`.

  • **Already `--!strict` but erroring**: skip conversion; just resolve the reported errors.

Why it's not find-and-replace

Luau can't infer fields through `setmetatable`, so strict mode turns that blind spot into errors. The fix is an explicit `export type` block enumerating every `self` field, plus dot-syntax methods that name `self`. Flipping the header without these just produces a wall of errors — supplying the types the checker can't infer *is* the job.

The verification loop (fast inner loop)

`luau-lsp analyze` is the same engine as `lint:luau`, pointed at one file (~2.5s). One-time setup if `sourcemap.json` / `globalTypes.d.lua` are missing at repo root: `npm run prelint:luau`.

luau-lsp analyze --sourcemap=sourcemap.json --base-luaurc=.luaurc \
  --defs=globalTypes.d.lua --flag:LuauSolverV2=false --ignore='**/node_modules/**' \
  src/<package>/src/<Realm>/<File>.lua

Clean = only the `[INFO] Loading...` line. `LuauSolverV2=false` is required (repo pins the old solver). **Never drop `--defs=globalTypes.d.lua` or `--base-luaurc=.luaurc`** — without `--defs` the analyzer loses the Roblox global declarations and reports *false* `Unknown global 'tick'/'time'` (and similar) errors while still resolving `game`/`Enum`, which looks like a real conversion bug but isn't. If you see unknown-global errors only on deprecated globals, you forgot `--defs`; confirm with `npm run lint:luau` (which always passes the flag) before "fixing" anything. Iterate until clean, then run `npm run lint:luau` **once** as the final gate — single-file analyze can't see files that depend on *yours*, and tightening a type ripples to subclasses and callers. Triage new downstream errors: pre-existing → leave & flag; your type is genuinely too tight for the real contract → loosen *your* type (often `T?` not `T`); a small obvious follow-on → fix it.

The gate is **both** `lint:luau` (types) **and** `lint:selene` (lints), which exits non-zero. Dot-syntax conversion routinely trips selene even when analyze is clean: an `unused_variable: self` (method body ignores the now-explicit param → rename it **`_self`**) or a `shadowing` from an Rx escape `local X = X :: any` inside a function (→ cast at the source, `local X: any = require("X")`). See the "selene" section in `references/conventions.md`.

Core patterns

**Class with a parent (the common case):**

--!strict
local require = require(script.Parent.loader).load(script)

local BaseObject = require("BaseObject")

local MyClass = setmetatable({}, BaseObject)
MyClass.ClassName = "MyClass"
MyClass.__index = MyClass

export type MyClass = typeof(setmetatable(
	{} :: {
		_serviceBag: ServiceBag.ServiceBag,   -- EVERY self field, with its type
		_enabled: ValueObject.ValueObject<boolean>,
	},
	{} :: typeof({ __index = MyClass })
)) & BaseObject.BaseObject   -- intersection pulls in inherited _maid, _obj, etc.

function MyClass.new(serviceBag: ServiceBag.ServiceBag): MyClass
	local self: MyClass = setmetatable(BaseObject.new() :: any, MyClass)
	self._serviceBag = assert(serviceBag, "No serviceBag")
	return self
end

**Methods use dot syntax with explicit `self`** (colon syntax loses the `self` type in strict mode). Callers still write `obj:Method()`; only the definition changes:

function MyClass.GetEnabled(self: MyClass): boolean
	return self._enabled.Value
end

⚠️ Metamethod classes — NEVER rewrite `rawget`/`rawset` or hoist `setmetatable`

The patterns above assume the ordinary metatable (`__index = MyClass`, default `__newindex`). Some classes define a **custom `__index` and/or `__newindex` function** that intercepts field access — often to expose computed keys (`.Value`, `.Changed`) and to **`error()` on any unknown key**. Grep for `(MyClass :: any).__index = function` / `.__newindex = function` before touching the constructor or any `self._field` access. For these classes, two edits that look like harmless cleanups are **runtime-breaking changes** — do not make them:

1. **Do not replace `rawget(self, "_x")` with `self._x`, or `rawset(self, "_x", v)` with `self._x = v`.** The raw calls are load-bearing: they deliberately bypass the custom metamethod. Routing the access through `self._x` fires the metamethod, which may compute a different value or `error("Bad index")` — silently for present fields, fatally for absent ones (e.g. lazy caches that start unset). Keep every `rawget`/`rawset` exactly as-is; the only allowed change is adding the receiver cast: `rawget(self, "_x")` → `rawget(self :: any, "_x")` (and `:: any` on the result if the checker complains about `any?`).

Read more
Stats
634
Stars
148
Forks
Active
Maintenance
Lua
Language
MIT
License
Last commit
Created

Repo: Quenty/NevermoreEngine

@quenty
Ships withnevermoreengine

ModuleScript loader with reusable and easy unified server-client modules for faster game development on Roblox

Stars on GitHub

Momentum

stars in total
628
Star history of Quenty/NevermoreEngine on GitHub, a chart by star-history.com

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc13d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about strict-typing-luau.

Which coding agents can use strict-typing-luau?
strict-typing-luau installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.