---
url: /strict-typing/modes.md
description: Strict, nonstrict, and nocheck compared, plus when each mode genuinely fits.
---

# Modes

*Written: {{ $frontmatter.date }}*

## Learning objectives

* You place the line-1 directive correctly.
* You state what strict, nonstrict, and nocheck each prove.
* You explain default any in nonstrict.
* You set the project default without hiding errors.
* You keep game code strict and isolate unchecked data files.

## Prerequisites

* [Annotations and Inference](./annotations-and-inference.md)
* [Syntax and Control Flow](../luau-basics/syntax-and-control-flow.md)

## The directive sits on line one

One line tells the checker how hard to look. Everything below follows that choice.

Write `--!strict`, `--!nonstrict`, or `--!nocheck` as the first line of the file. The [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide lists exactly these three modes. The [luau.org types](https://luau.org/types) intro lists the same three spellings.

A misspelled directive behaves like no directive. The checker does not warn you about the typo from inside the type system. Spell the line exactly.

Start every game file with the intended mode. Reviewers see intent before logic. Mode mistakes surface in review, not in playtests.

Put this Script in ServerScriptService.

```luau
--!strict
local coins = 10
local doubled = coins * 2
```

This block checks clean in strict. You infer `coins` as `number` and `doubled` as `number`. The header proves you asked for full checking.

## Nonstrict fills gaps with any

When the checker cannot tell, nonstrict lets the code pass. You move fast and miss some bugs.

In nonstrict, an unannotated variable with no inferable type gets `any`. The [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide states this default directly. The [luau.org types](https://luau.org/types) intro shows an uninitialized variable inferring `any` and letting a bad addition pass.

`any` disables checks on that value. Bad arithmetic, bad calls, and bad field reads pass silently. You find out at runtime.

Use nonstrict while sketching or while migrating old code. Move each gameplay file to strict as soon as you can annotate its edges.

Put this Script in ServerScriptService.

```luau
--!nonstrict
local stash
stash = 1
local out = stash + "hello"
```

This block checks clean in nonstrict. You infer `stash` as `any` at declaration, per the [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide and the [luau.org types](https://luau.org/types) intro. The addition passes without a type error. At runtime the addition fails. That gap is why you do not keep gameplay in nonstrict.

Put this Script in ServerScriptService.

```luau
--!strict
local stash: number = 1
local out = stash + "hello"
```

This block errors in strict. The checker reports that a string cannot convert to number. You fix it by converting the string first or by not adding mismatched types. The same logic that passed in nonstrict now fails fast.

## Strict tracks types across statements

Strict remembers what you assigned. Later lines must respect earlier facts.

The precise rule is flow plus inference. Strict infers locals from assignments and checks every use against that flow. The [luau.org types](https://luau.org/types) intro shows strict catching a string-plus-number bug that nonstrict missed. The [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide describes strict as asserting all types from inference or annotations.

You must annotate boundaries and narrow uncertain values. Generated tables and loosely shaped data need extra work.

Autocomplete improves. Refactors break loudly in the editor. You ship gameplay with fewer runtime type crashes.

Put this Script in ServerScriptService.

```luau
--!strict
local rate = 4
rate = "fast"
```

This block errors in strict. You infer `rate` as `number` from the first assignment. The second assignment tries to store a string. The checker rejects the change. You fix it with a new variable or a correct number.

## Nocheck skips the checker

Some files are data, not logic. Checking them adds noise without safety.

The precise rule is full opt-out. `--!nocheck` disables type inference feedback for the file. The [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide defines nocheck as no checking. The [luau.org types](https://luau.org/types) intro shows a bad addition passing silently under nocheck.

No errors, no inference, no autocomplete help. A typo in a data file stays hidden until runtime reads it.

You reserve nocheck for generated files and raw data dumps. You keep every hand-written gameplay file in a checked mode. You never spread nocheck to hide a logic error.

Put this ModuleScript in ReplicatedStorage.

```luau
--!nocheck
local raw = {}
raw.coins = 10
raw.coins = "many"
```

This block passes with no diagnostics because nocheck is active. You infer no reliable types here. Runtime sees the last assignment win. You use this only for machine-written data you do not hand-edit.

## You set the project default once

New scripts should start strict. Old scripts keep their headers until you migrate them.

The precise rule is a Workspace setting. You set the type-check default on the Workspace object so fresh scripts inherit your choice. The [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) guide points to the Workspace property for this default. The per-file header still wins when present.

The setting does not rewrite existing headers. A file with `--!nonstrict` stays nonstrict until you change that line. Keep every header exact on line one; an unrecognized header gets no documented fallback, so a header that does not take effect means a typo to fix.

You set the project default to strict for game code. You migrate legacy files one by one. You track remaining nonstrict files as tech debt.

Put this Script in ServerScriptService.

```luau
--!strict
local Workspace = game:GetService("Workspace")
local mode = Workspace.LuauTypeCheckMode
```

This block checks clean. You infer `mode` as the enum value that stores the default. You read it to confirm your Studio default. You change the default in Studio settings, not by editing this read.

## You pick strict and keep it

Strict fits game code. Nonstrict fits a sketch. Nocheck fits generated data.

Strict fits Services, Controllers, shared logic, and net boundaries. Nonstrict fits a quick sketch you plan to tighten. Nocheck fits generated or bulk data files you do not reason about.

Downgrading a file hides the error without fixing the bug. The bug still ships. The next reader inherits a weaker file.

You never downgrade to silence strict. You annotate, narrow, or remodel instead. Later chapters give you those tools.

Put this Script in ServerScriptService.

```luau
--!strict
local function payout(coins: number): number
	return coins * 2
end
local got = payout(6)
```

This block checks clean. You infer `got` as `number`. This is the shape you want for gameplay. Params and returns are explicit. Calls are proven at edit time.

## Common mistakes

Wrong: you place the directive below code.

Put this Script in ServerScriptService.

```luau
--!nonstrict
local coins = 1
```

This header is correct only because it sits on line one. Move it to line two and the checker ignores it. You fix placement by keeping the mode line first with no blank line above it. The fixed block checks clean in its stated mode. You infer `coins` as `number`.

```luau
--!strict
local coins = 1
```

This fix checks clean. You infer `coins` as `number`.

Wrong: you downgrade to hide a mismatch.

Put this Script in ServerScriptService.

```luau
--!nonstrict
local total = 10 + "5"
```

This block can pass in nonstrict while still failing at runtime, which is the documented nonstrict gap: untyped locals skip the check the strict solver would run. You hide the bug instead of fixing it. You fix it by staying strict and converting openly.

```luau
--!strict
local total = 10 + tonumber("5")
```

This fix is wrong as written because `tonumber` returns an optional. The honest strict fix needs a nil guard, which you learn in [Narrowing and Guards](./narrowing-and-guards.md). The lesson here is mode choice: you stay strict and fix the value.

Wrong: you put game logic in nocheck.

Put this Script in ServerScriptService.

```luau
--!nocheck
local price = 20
local charge = price + "fee"
```

This block reports nothing because checking is off. Runtime still fails on the bad addition. You fix it by moving logic back to strict and typing the values.

```luau
--!strict
local price: number = 20
local fee: number = 5
local charge = price + fee
```

This fix checks clean. You infer `charge` as `number`.

## Exercises

1. Write the same bad addition in all three modes and note which modes flag it.
2. Show nonstrict default any with an uninitialized local.
3. Fix a strict reassignment error without changing the mode.
4. Mark a generated data module nocheck and explain why logic must not live there.
5. Confirm your project default and state which mode new gameplay files use.

::: details Solution 1
Put this Script in ServerScriptService.

```luau
--!strict
local bad = 1 + "x"
```

This strict sample errors. The checker rejects string-to-number conversion. Change the header to `--!nonstrict` or `--!nocheck` and the diagnostic disappears while the runtime bug remains. That contrast proves why gameplay stays strict.
:::

::: details Solution 2
Put this Script in ServerScriptService.

```luau
--!nonstrict
local pending
pending = 2
```

This block checks clean in nonstrict. You infer `pending` as `any` at first. Add `pending + "oops"` and nonstrict still passes while strict on the same code would flag the mix. You annotate `pending` as `number` to close the hole.
:::

::: details Solution 3
Put this Script in ServerScriptService.

```luau
--!strict
local speed: number = 6
local speedLabel = tostring(speed)
```

This block checks clean. You infer `speedLabel` as `string`. You keep the number intact and derive a separate string. You do not reassign `speed` to a string.
:::

::: details Solution 4
Put this ModuleScript in ReplicatedStorage.

```luau
--!nocheck
local dump = {}
dump.count = 3
```

This block passes unchecked. You infer no reliable type. You keep this pattern only for machine output. You require hand-written logic to live in strict files that validate this data on load.
:::

::: details Solution 5
Put this Script in ServerScriptService.

```luau
--!strict
local Workspace = game:GetService("Workspace")
local current = Workspace.LuauTypeCheckMode
local strictReady = true
```

This block checks clean. You infer `current` as the Workspace default value and `strictReady` as `boolean`. You set the Studio default to strict for new gameplay. You migrate old files file by file.
:::

## Summary

* The mode line sits on line one and selects the proof level.
* Nonstrict defaults unknown locals to any and misses bugs.
* Strict tracks flow and rejects mismatches early.
* Nocheck fits generated data only.
* You set the Workspace default to strict and never downgrade to hide errors.
* Next: [Narrowing and Guards](./narrowing-and-guards.md).
