Try it in your browserpreview Source

Casting and annotations✎

Where a pin's type ends and Luau's begins: casts, declared types, and what gets written.

Verified

There are two type systems here and they are not the same size. A pin type is Roswaal's: one name, used to decide what may be wired to what and what colour to draw it. A Luau type is whatever Luau can say — unions, optionals, table types, functions, generics. Everything on this page is one of the places the second one reaches the file.

Casting

Cast takes any Luau type expression verbatim, so an intersection, a union or a table type all work — its Type pin is typed in rather than wired, because the text becomes part of the generated code. Its picker lists this graph's own types first, then those a required module exports.

All three are drawn as pills, the shape the comparisons, the arithmetic and and / or use: the value and the type down the left, the symbol in the middle, the result on the right. A cast is an operator, and the shape is the point — a claim made without a check is worth spotting at a glance rather than after reading a header.

Shows, in the Inspector, swaps the :: for the node's name where that reads better — :: is Luau's own and is the one symbol here nobody arrives already knowing. It is stored on the node, because it sets the pill's width; New cast nodes in Settings decides what a cast you drop today starts as.

The Type pin is a list you pick from: Luau's own types, then Roblox's datatypes, then every Instance class, grouped as the class picker groups them. Still only a suggestion — whatever you type is committed, which is how an intersection like Model & { Humanoid: Humanoid } is written.

BasePart::
BasePart:: { }
BasePart:: any ::
The three of them. The type is picked from the list or typed, and either way it becomes text in the generated file rather than a value at runtime.
luau
local humanoid = (character :: Model & { Humanoid: Humanoid }).Humanoid
NodeWritesFor
Cast(value :: T)Saying what a value is, when you know and the typechecker does not.
Cast Array(value :: { T })A collection you know more about than its type says — Get Descendants is { Instance }.
Cast Through Any((value :: any) :: T)Two types Luau will not convert between directly. The any in the middle is the claim being made twice.

Warning

:: is a claim, not a check — nothing tests it at runtime. Ask with Is A first, which narrows the type for its branch.
Is AInstanceBasePartClass Name
Is A asks the question a cast assumes the answer to. Branch on it, and cast inside the arm where it is true.

Where the cast is written

A cast is the one value node whose line can be the point. Cast in the Inspector chooses which of three:

CastWritesFor
AutomaticA line once two things read itThe default, and the rule every pure node follows. One reader gets it spliced; two get local part = value :: BasePart and then read part.
ExplicitAlways a lineSeveral statements below read it and you would rather see the claim written once, above them, than repeated at each use.
ImplicitNever a lineThe assertion is spliced where it is used — and dropped entirely where Luau has already narrowed the value itself.

An implicit cast inside an Is A branch disappears

Luau narrows a value for the length of the arm that tested it. Inside if part:IsA("BasePart") then, part is a BasePart as far as the typechecker is concerned, and a cast there tells it nothing it does not know. An implicit Cast in that arm therefore writes nothing at all and hands the value through, which is what the hand-written Luau does too.

luau
for _, part in character:GetDescendants() do
	if part:IsA("BasePart") then
		-- an implicit Cast to BasePart here writes nothing
		part.Transparency = 1
	elseif part:IsA("Decal") or part:IsA("Texture") then
		-- and here, a cast to `Decal | Texture` writes nothing either
		part.Transparency = 1
	end
end

Two classes tested with Or narrow the value to either of them, so the claim that matches is the union — Decal | Texture — and that is the one that disappears. A cast to only one half stays, because the Or did not prove it. And narrows everything both sides tested, since both hold.

Info

The match must be exact: the branch's classes and the cast's must be the same set, or the cast is written. Settings → Project → Casts proved by a subclass also leaves one out when the branch proved a derived class — IsA("Part") for a cast to BasePart.

Warning

The narrowing applies only in the True arm. In the False arm, or after the end, the cast is written out.

Declaring a type

Declare Type at Top writes above everything else; Declare Type writes where the node sits, which is what a type built from typeof needs, because Luau reads a file in order. Both take three shapes:

Declare Type at Top
Declare TypeValue
The hoisted one has no pins at all — it declares rather than runs. The in-flow one sits in the execution chain, and shows a Value pin only for the typeof shape.
ShapeWritesWhen
Table of Fields{ walkSpeed: number, weld: WeldConstraint? }A record. The fields are rows in the Inspector, so a brace cannot go missing. Layout writes it on one line or one field to a line, as Make Dictionary does.
Custom LuauWhat you write in the code editor, checked as a typeA union, a function type, a generic — everything the row editor cannot say.
Type of a Valuetypeof(Tuning)The type of something the file already has. Declare Type only, since a hoisted type is written above every value there is.

Info

A declared type is exported unless you untick it, so other graphs can name it. Exported inside a branch, loop or function, it is refused: export type is top-level only.

Reading what a type holds

Get Member reads a field off a value whose type declares one: wire the value in and its type's fields are the list. The result is typed as the field is, so aim on the type above gives a Vector3 pin and the wire from it is a Vector3's colour.

It is one line — the access it writes, one input, one output — and which member it reads is chosen in the Inspector. Type input. into either node search and the members of everything the graph names are there: picking one places the getter and the Get Member on it, wired.

Drag a wire out of a typed pin and that type's members are in the menu under their own heading — a Part's properties, a declared type's fields. Picking one places a Get Member already wired to the pin you dragged.

.…
Get FieldObjectnameField
Get Member offers what the type declares; Get Field reads any key you name.
Where the members come fromWhen
A type this graph declaresA Declare Type of either kind, entered as fields or written as a table.
A type a required module exportsConfig.Tuning, read from the graph that declares it.
A Roblox classBasePart.Position, Humanoid.WalkSpeed. Roblox graphs only — a Lune program has no instances.

A node that names a class hands back that class. New Instance set to Part gives a Part, not an Instance, and so do Get Service and the Find First Child and Ancestor nodes that take a Class Name. That is what puts a class's own properties in Get Member's list without a Cast first. The Find First nodes can find nothing, so in Nonstrict and Strict the file casts to Part?. A Class Name wired from a String keeps its class; from anything else, it is an Instance.

Warning

Use Get Field for a table whose keys come and go. A type without fixed fields offers no members, and a member this graph's own type lacks is refused.

What Roswaal writes for you

Annotations follow the graph's typechecking mode, in the tools along the top of the canvas. Default writes no mode line and no annotations; Nonstrict and Strict write both. So a type you set is a type that appears — in the two modes that asked for types at all.

Declare LocalLocalNameValue
Get Local
Declare Local carries the type, and shows it under its title once set. Get Local reads the value by name, with its pin taking the type's own colour.
Set onComes out as
A variable, in the Variables panellocal health: number = 100
A variable set to constconst health: number = 100
A Declare Local, in the Inspectorlocal restores: { [Model]: Restore } = {}
A function's parameters and returnslocal function read(tank: Model): Config
A node that produces a valuelocal part: BasePart = ...

A type that is more than a name is written as itself. { [Model]: Restore }, Model? and (number) -> string used to come out as any with nothing said about it; they are written as typed now, and a mistake in one is Luau's to report with a line number. Text that is plainly not a type — two words, an unclosed brace — still becomes any, because writing it would break the file rather than the line.

Choosing one

Everywhere a type is chosen — a variable, a parameter, a local, a field of a declared type — the control is the picker: the same window the Class Name pins and the casts open. Search at the top, everything under it, grouped by where each type comes from.

The headings are ordered by how close to hand they are: this graph's own declared types first, then the types a required module exports, written as you would write them (Config.Tuning), then Luau's own, then Roblox's values — and after those the instance classes, grouped the way the engine groups them.

Whatever you type is taken, listed or not, which is how a type the list could never hold is set: { [Model]: Restore }, Model?, (number) -> string. Clearing it means any.

Info

Declare a long type once, then pick it by name: Custom Luau on a Declare Type for a union or function type, or Type of a Value for typeof(Tuning). It appears under This graph in every picker.

The Variables panel lists those same types under Types. Drag one onto the canvas for a Declare Local of that type, or hold Ctrl for a Cast to it — the same Get-or-Set convention a variable follows.

What a pin type still decides

A wire is allowed when the two pins agree, when either is any or wildcard, between number and string because Luau converts those itself, and from an instance class to an Instance pin — a Model goes anywhere an Instance is wanted. The editor and the compiler ask the same question, so a wire the canvas accepts is never one the compile complains about.

Warning

The other direction needs a Cast. Drop an Instance wire on a Model pin and one is placed for you, so the claim shows in the graph. Two unrelated classes are refused, with the reason.

Last reviewed 12 September 2026 by @neopolitans