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
6041 skill
Install
$ npx -y skills add Quenty/NevermoreEngine --skill strict-typing-luau --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/strict-typing-luau

Context preview

The summary Claude sees to decide when to auto-load this skill.

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",

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
Ships withnevermoreengine

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

Get the whole plugin
Stats
603
Stars
144
Forks
Active
Maintenance
Lua
Language
MIT
License
1d ago
Last commit
12y ago
Created

Repo: Quenty/NevermoreEngine