Blog/Roblox/🎮Game Guides

Roblox Luau Type Checking: nocheck, nonstrict, strict

Roblox says every script used to default to no typechecking. That is no longer how it works: a Workspace property now sets the default mode, projects using only nocheck and nonstrict were moved to the New Type Solver automatically, and strict mode projects were not. Here is what each mode reports, what the new solver changed, and the annotation syntax that covers most code.

Published August 29, 2026·12 min read·By Mythras
Roblox Studio Script Editor showing Luau code with two problems underlined in orange — a return of the undefined name newPar, and a malformed for loop.

Type checking in Roblox is easy to file under optional tidiness — a per-file switch you flip when you feel like it. Roblox's own general release announcement for the New Type Solver puts that arrangement in the past tense: "Historically, every script defaulted to no typechecking and creators could provide script directives like --!strict and --!nonstrict to set the mode in each file."

That sentence is in the past tense on purpose. Two things in it stopped being true in the same release: the default is now a per-project property rather than a fixed engine behaviour, and the inference engine underneath the checker was swapped out for projects that were not already using strict mode. If your mental model of Luau typing is "add --!strict if you want red squiggles," it predates both.

The default changed, and your old scripts did not notice

On 20 November 2025, Roblox announced that Luau's New Type Solver was moving out of Studio Beta. Two things shipped together in that release, and it is worth separating them because they are independent switches:

  1. A new type inference engine — "The New Type Solver is moving out of Studio Beta, and will be enabled by default for all users in nocheck and nonstrict modes! Strict mode users can continue to opt in voluntarily."
  2. A redesigned nonstrict mode, which the announcement describes as "enabled by default for everyone with a focus on reporting only definite runtime errors to catch bugs earlier and improve the developer experience."

Roblox also gave the Studio Beta toggle people had been using for this an end date, saying it "will be removed in the first release of the new year on January 7th, 2026". Its instruction to beta users was specific: if you had been running the New Type Solver in strict mode through the beta, set UseNewLuauTypeSolver from Default to Enabled in your experience's Workspace to keep it. "All other users should be migrated automatically."

Script directives did not change. --!strict at the top of a file still wins for that file. What changed is what a file gets when it says nothing at all.

Roblox Studio Script Editor with two problems underlined in orange: a return of an undefined name, and a for loop missing its 'in' keyword

The three directives, and what each one does

The directive goes on the first line of a script. Roblox's type checking documentation and the upstream Luau docs describe the same three modes in slightly different words, and both descriptions are useful because they are aimed at different questions — what the checker asserts, and what it infers.

DirectiveRoblox docs descriptionLuau docs description
--!nocheck"Don't check types.""completely disables the type inference engine for the file"
--!nonstrict"Only asserts variable types if they are explicitly annotated."the checker "is more forgiving": if it cannot work out a type early on, "we infer the type could be anything (the any type)"
--!strict"Asserts all types based off the inferred or explicitly annotated type."Luau "is smarter about tracking types across statements"

Two cautions on that table, because it is the part most likely to mislead.

First, the Roblox page carries an alert at the top pointing readers elsewhere: "For the latest and most complete type checking documentation, see [here]" — linking to luau.org. Treat luau.org as the authority on language behaviour and create.roblox.com as the authority on the Studio integration.

Second, both of those pages describe inference by mode, and the New Type Solver announcement says that is no longer how it works: "Instead of varying the type inference rules based on mode, we've written a new implementation with one shared type inference pass between each mode with separate error reporting implementations. These error reporters, called typecheckers, define sets of rules for what script analysis warnings get produced." Under the new solver, the mode selects a reporter, not an inference strategy. Neither docs page had been rewritten to say that at the time of writing, so if the two sources look like they disagree, that is why.

Whichever mode you pick, the output lands in the same two places. Roblox's documentation: type mismatches "are highlighted in the Script Editor and surfaced as warnings in the Script Analysis window," which you open from the Analysis button in Studio's Script tab toolbar.

Roblox Studio Script Analysis window listing one error and one warning: a syntax error at 8,7 and UnknownGlobal at 5,9 for the name newPar

Which solver you are running right now

This is the question the release actually turns on, and the announcement's own FAQ answers it: "If you use nocheck or non-strict mode for all of your scripts, you will be automatically moved to the New Type Solver with nonstrict enabled. If you use strict mode, you will remain on the old solver by default, but can opt-in via Workspace Properties."

Your scriptsSolver you get by defaultHow to change it
All --!nocheck / --!nonstrictNew Type SolverSet UseNewLuauTypeSolver to Disabled to opt out
Any --!strict in the projectOld type inference engineSet UseNewLuauTypeSolver to Enabled to opt in

The reason strict mode was held back is stated rather than hinted at: Roblox says there are "still a number of issues preventing existing projects from adopting the New Type Solver, as well as some intentional work that creators will need to do to fix real type errors that went uncaught because of the limitations of the old system."

The old engine is not gone. Roblox committed to "keep the old type inference engine available through 2026 to give everyone time to migrate at a reasonable pace," and separately said of the opt-out property: "We will retain this property in Studio for at least one year and until we've entirely sunset the old Luau type inference engine."

What the new nonstrict mode warns about

If you have never annotated a line of Luau in your life, this is the section that matters, because nonstrict is what you are now getting by default and Roblox designed it for exactly that reader. The stated goal: nonstrict should "give clear, useful developer feedback during editing without requiring annotations and without requiring the creator to understand type systems."

The announcement names two flavours of warning it currently produces:

  1. "simple lint-style warnings like unknown symbols"
  2. "circumstances where we can prove that the code will behave nonsensically at runtime"

The first is the typo catcher. Roblox's own example:

local dog = { name = "molly", age = 13 }
local cat = { name = "athena", age = 2 }

function pet(animal)
  print(`pets for {animal.name}`)
end

pet(dig) -- oops! unknown global: dig
pet(cat)

That is the same UnknownGlobal warning class visible in the Script Analysis screenshot above.

The second is narrower than it sounds, and the pair of examples Roblox gives is the clearest statement of where the line sits. This warns:

function foo(x)
  math.abs(x)
  string.lower(x)
end

Roblox's reasoning: "there is no possible way to call the function without causing a runtime error. If we pass anything other than a number, math.abs will fail, and if we pass anything other than a string, string.lower will fail."

This does not warn:

function bar(x)
  if math.random(2) == 1 then
    math.abs(x)
  else
    string.lower(x)
  end
end

Because it "might work." Roblox describes the mode as "very permissive right now as a foundation for continued work." If you want the checker to argue with you about types you have not annotated, nonstrict is not that mode and is not trying to be.

The two Workspace properties

The Roblox Studio window, with the Explorer panel listing Workspace and the other services on the right and the Properties panel docked beneath it.

Both live under a Scripting category on Workspace. Neither is meant to be flipped from a running script — LuauTypeCheckMode is documented with PluginSecurity on both read and write, and UseNewLuauTypeSolver is tagged NotScriptable. You set them in the Properties panel in Studio, per project.

LuauTypeCheckMode sets the default mode

Workspace.LuauTypeCheckMode "Controls the Luau type checking mode applied to scripts in the experience," and takes Enum.LuauTypeCheckMode, which has four members:

ValueNumberWhat it means
Default0"Uses the engine-default type checking mode."
NoCheck1"Scripts are not type-checked."
Nonstrict2"Scripts are type-checked in nonstrict mode."
Strict3"Scripts are type-checked in strict mode."

Per-file directives still override it. Roblox: "script directives continue to work as before, but the default mode will be determined by LuauTypeCheckMode."

Two details worth keeping. Roblox said it would be "setting Nonstrict as the initial value of this property in Roblox-provided templates" — so a project started from a Roblox template is not starting from nothing. And the property is not a migration aid due to expire: "This setting will continue to exist in some form in perpetuity as we believe that individual creators are best able to determine the typechecking mode best suited for themselves, their workloads, and their teams."

UseNewLuauTypeSolver picks the engine

Workspace.UseNewLuauTypeSolver "controls whether the new Luau type solver is used for type inference and type checking in scripts." The docs are explicit that the two properties are orthogonal: LuauTypeCheckMode "controls the mode (strict, nonstrict, etc.) while this property controls which solver implementation is active."

It takes Enum.RolloutState, a three-value pattern Roblox reuses across engine features:

ValueNumberWhat it means
Default0"Uses the engine-wide rollout default, which changes as the feature progresses through its rollout phases."
Disabled1"Opts out of the feature regardless of the engine-wide rollout phase."
Enabled2"Opts in to the feature regardless of the engine-wide rollout phase."

The enum's own description explains why Default is a moving target: a feature is "initially opt-in (Default equals disabled), then opt-out (Default equals enabled), and finally always on." So Default today is not a promise about tomorrow — if you need a fixed answer for your project, set Enabled or Disabled explicitly rather than leaving it on Default.

Setting it to Enabled is a team-wide decision, not a personal one: "every person working on your experience will be using the New Type Solver while editing that place. With this on, the New Type Solver will take over powering autocomplete, hover type, and script analysis warnings for the whole project."

Annotations: the syntax that covers most code

Annotation is the : operator after a name. Nothing here is new with the solver — it is the same surface you have been able to use since Luau shipped types, and it is what nonstrict keys off when it says it "only asserts variable types if they are explicitly annotated."

local foo: string = "bar"
local x: number = 5

Roblox's docs name four primitive types for annotations: nil (no value), boolean, number and string. Beyond those, "all classes, data types, and enums have their own types that you can check against":

local somePart: Part = Instance.new("Part")
local brickColor: BrickColor = somePart.BrickColor
local material: Enum.Material = somePart.Material

A trailing ? makes a type optional — the variable "can be either the specified type ... or nil":

local foo: string? = nil

You can also pin a string or boolean to a literal value rather than the general type:

local alwaysHelloWorld: "Hello world!" = "Hello world!"
alwaysHelloWorld = "Just hello!"  -- Type error
local alwaysTrue: true = false    -- Type error

When inference is too generic, cast with ::. The rule Luau enforces is that the cast is itself checked: casts require "that one of the conversion operands is the subtype of the other or any," which is what makes the middle line below legal and the last one not:

local myNumber = 1
local myString: string

myString = myNumber          -- Not OK; type conversion error
myString = myNumber :: any   -- OK; all expressions can be cast to 'any'
local myFlag = myNumber :: boolean  -- Not OK; types are unrelated

One more cast rule, stated in a single line on luau.org: when you cast a variadic or a multiple-return call, "only the first value will be preserved, and the rest discarded."

Functions, tables and variadics

Parameter and return annotations are the highest-value place to spend effort, because they are what let the checker reason about call sites rather than one line at a time:

local function add(x: number, y: number): number
	return x + y
end

add(5, 10)
add(5, "foo")  -- Type error: string could not be converted into number

Multiple returns go in parentheses, and a function type on its own is written (in) -> out:

type add = (x: number, y: number) -> number
type FindSource = (script: BaseScript, pattern: string) -> (string, number)

Luau has no table type. Table shapes are written with {}{type} for a list, {[indexType]: valueType} for an index, or explicit string keys:

local numbers: {number} = {1, 2, 3, 4, 5}

local numberList: {[string]: number} = { Foo = 1, Baz = 10 }
numberList["bar"] = true  -- Type error: boolean can't convert to number

type Car = {
	Speed: number,
	Drive: (Car) -> ()
}

This matters more than it looks, because Luau's type system "is structural by default, which is to say that we inspect the shape of two tables to see if they are similar enough." A table that matches the shape of Car is a Car; there is no nominal declaration to satisfy.

Variadics annotate the ... directly in a function, but need different syntax in a type. Both forms, because mixing them up produces a confusing parse error:

local function addLotsOfNumbers(...: number)  -- in a function: ...: type

type addLotsOfNumbers = (...number) -> number -- in a type: ...type

Writing (...: number) -> number in a type alias gets you Expected type, got ':'.

Unions, intersections, typeof and generics

| is union, & is intersection:

type numberOrString = number | string
type type1 = {foo: string}
type type2 = {bar: number}
type type1and2 = type1 & type2  -- {foo: string} & {bar: number}

typeof lets you derive a type from a value instead of writing it out, which is the practical way to type an existing table without transcribing every field:

type Car = typeof({
	Speed = 0,
	Wheels = 4
})  --> Car: {Speed: number, Wheels: number}

It is also the documented route to a metatable type, which is what you need to type a metatable-based object of the kind Lua-style OOP builds in ModuleScripts:

type Vector = typeof(setmetatable({}::{
	x: number,
	y: number
}, {}::{
	__add: (Vector, Vector|number) -> Vector
}))

Generics are type parameters, written <T>, and they work on both aliases and functions:

type List<T> = {T}
type Map<K, V> = {[K]: V}

local function State<T>(key: string, value: T): State<T>
	return { Key = key, Value = value }
end

local Activated = State("Activated", false)     -- State<boolean>
local TimesClicked = State("TimesClicked", 0)   -- State<number>

Sharing types between scripts

A type declared in a ModuleScript is local to it until you export it:

-- ReplicatedStorage/Types
export type Cat = {
	Name: string,
	Meow: (Cat) -> ()
}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(ReplicatedStorage.Types)

local newCat: Types.Cat = {
	Name = "metatablecat",
	Meow = function(self)
		print(`{self.Name} said meow`)
	end
}

This is the piece that makes typing worth the effort across a real project rather than one file: a shared Types module gives the client and the server the same names for the same payloads, which is exactly the boundary RemoteEvents blur.

What the new solver added

The announcement lists five improvements the rewrite enabled. Quoting the descriptions rather than paraphrasing them, because several are easy to overstate:

ImprovementWhat Roblox says it does
Read-only table properties"Inferred and annotated table properties can now be read-only, enhancing type safety."
Better type refinements"The type system now tracks variable type changes dynamically and more intelligently, reducing instances where the logic of the program obviously contradicts the error"
Type functions"Enables better inference for overloaded operators and table properties, increasing accuracy, and provides users a way to write highly expressive type signatures for code"
Singleton type improvements"Improved handling of singleton types reduces spurious warnings and the need for excessive casting."
Relaxed casting rules"Casting rules are more permissive, reducing the need to cast through any when trying to cast to a specific type."

Type functions are the genuinely new language feature in that list. The Luau documentation defines them as "functions that run during analysis time and operate on types, instead of runtime values," able to "use the types library to transform existing types or create new ones." They run in a restricted environment — the docs enumerate what is available alongside the types library: assert, error, print, next, ipairs, pairs, select, unpack, getmetatable, setmetatable, rawget, rawset, rawlen, raweq, tonumber, tostring, type, typeof, and the math, table, string, bit32, utf8 and buffer libraries.

Directives that look similar and are not type checking

--!native sits on the same first line and is a completely different feature. It turns on native code generation, which compiles server-side scripts "directly into the machine code instructions that CPUs execute, rather than regular bytecode that the Luau VM operates on." Roblox's documentation is clear that this "enables native code generation for all functions in the script, and the top-level scope, if deemed profitable," and that "behavior of the natively executing scripts is exactly the same as before and only the performance is different."

There is also a per-function @native attribute if you want one hot function compiled rather than the file. Neither changes what the type checker reports, and neither is a substitute for the parallel Luau work if what you actually need is more threads.

The traffic between the two features runs one way. Roblox documents that native code generation reads your annotations: "Luau type annotations on function arguments are checked," with Vector3 arguments singled out as especially worth annotating, because the compiler guesses a type for each variable and "mispredictions may trigger unnecessary checks, resulting in slower code execution." Annotations can therefore make a --!native script faster — but the directives themselves stay independent, and typing a script that is not native changes nothing at runtime.

What the Luau team says is still rough

Roblox published the known issues alongside the release rather than after the complaints, and prefaced all three with "We are actively working on several known issues, particularly for strict mode":

  • Error message quality. "In some cases, the New Type Solver can generate types and error messages that are larger and more verbose, or otherwise less clear than the old system."
  • Performance. "The New Type Solver is still slower than we'd like in some cases, which can sometimes lead to excessive memory usage in Roblox Studio."
  • Correctness bugs. "We're also aware of a number of places where strict mode type-checking is currently too strict, and are working to address them."

And the line that decides how much of this you need to care about, from the same section: "None of these issues can affect your experience in production, the type system only runs at edit time in Roblox Studio." Nothing in this article changes a single byte of what your servers run. A type error is a message to you, not a behaviour change.

Quick Action Checklist

  • Check Workspace.LuauTypeCheckMode in your project before assuming anything about defaults — a file with no directive now inherits that property, and Roblox said it would set Roblox-provided templates to Nonstrict.
  • Leave UseNewLuauTypeSolver on Default only if you are comfortable with it changing as the rollout advances; set Enabled or Disabled when you want a fixed answer for the whole team.
  • If your project has any --!strict scripts, you are on the old engine by default. Opting in is a deliberate act, and Roblox says to expect real type errors the old system missed.
  • Use --!strict per file to migrate incrementally, rather than flipping the whole project's default mode at once.
  • Annotate function parameters and returns first — they buy the most checking per character.
  • Reach for :: only when inference is genuinely too generic; a cast still has to be to a subtype, a supertype, or any.
  • Write ...: number inside a function and ...number inside a type alias. They are not interchangeable.
  • Put shared shapes in an export type module so the client and server agree on the same names.
  • Read type-mode behaviour off luau.org, which Roblox's own docs page names as the current reference, and read the Studio settings off create.roblox.com.
  • Remember --!native is a separate directive that changes execution rather than checking, that native code generation does read your annotations, and that the type checker itself runs only at edit time.

Frequently Asked Questions

It is set per project by the Workspace.LuauTypeCheckMode property rather than being fixed by the engine. Roblox's November 2025 New Type Solver general release announcement states that historically every Roblox script defaulted to no typechecking, and that with the update the default mode is determined by LuauTypeCheckMode. Roblox also said in that announcement that it was setting Nonstrict as the initial value of the property in Roblox-provided templates, which would make a project started from a Roblox template begin in nonstrict rather than unchecked. Per-file directives such as --!strict continue to override the property for that file.

Keep Reading

Sources & Further Reading
Last updated August 29, 2026.

Related Guides

The Roblox Studio interface where server-side text filtering scripts are written, showing the 3D viewport, the Explorer tree of services, and the side panels used for scripting.
🎮Game GuidesAug 19, 2026·10 min read

Roblox Text Filtering: FilterStringAsync Done Right

Chat is filtered for you. Pet names, sign text, shop names and anything you pull off an external API are not — and Roblox documents that it takes games down until filtering is added. Here is the actual API, the getter that is now dead, and the pattern that ships.

Read article
A Roblox landscape built with voxel terrain — red-toned rolling hills dotted with a few bare trees, dark mesas along the horizon under a blue sky — from the official Roblox Creator Documentation.
🎮Game GuidesAug 8, 2026·11 min read

Roblox Terrain Editor: Build a World in Minutes

Terrain is the fastest way to turn an empty baseplate into somewhere worth standing. Generate makes a mountain range in one click, the brush tools carve it into a level, and none of it requires a single mesh.

Read article
Roblox Studio in its dark theme showing the 3D viewport, the Explorer tree, and the Properties window where InputContext, InputAction, and InputBinding instances are configured.
🎮Game GuidesJul 31, 2026·13 min read

Roblox Input Action System Guide: Bind Once, Ship Every Platform

Roblox quietly shipped the thing input code has needed for a decade: actions and bindings you configure in the Explorer instead of a LocalScript full of if-statements. One InputAction called CharacterSprint, three InputBindings — LeftShift, ButtonY, an on-screen button — and your script connects to Pressed and Released without ever asking what device the player is holding. Here's the full setup, the five action types, the threshold and Scale numbers that matter, and where UserInputService still earns its keep.

Read article
Roblox Studio in its dark theme showing the 3D viewport, the Explorer tree, and the Properties window where the Tags and Attributes sections used by CollectionService live.
🎮Game GuidesJul 28, 2026·13 min read

Roblox CollectionService Guide: Tag Once, Script Everything

If your Explorer has forty copies of the same killbrick script, you don't have a game — you have forty bugs waiting to disagree with each other. CollectionService fixes that: tag the objects, write one handler, done. Here's the exact pattern, the attribute layer that makes each tagged object configurable, the cleanup step most tutorials skip, and the replication behavior that quietly eats client-side tags.

Read article
Roblox experiences carousel showing game tiles including Driving Empire, Adopt Me, Field Trip Z, and DOORS fanned around a featured racing game.
🏆Tier ListsMay 29, 2026·11 min read

Best Roblox Games to Play in 2026

Roblox's front page is engagement bait. This is the filtered version: the games with real, sustained player counts and actual staying power, sorted by what you're in the mood for.

Read article
Roblox in-game Buy Item dialog showing an item priced in Robux with a subscription discount applied to the purchase.
🎮Game GuidesMay 29, 2026·11 min read

How to Get Robux Safely (Legit Ways + Scams to Avoid)

There is no free Robux generator. There never was. Here are the actual legit ways to get Robux without overpaying, the earning methods that really work, and the scams that exist purely to steal your account.

Read article