GDScript is gradually typed. You can mix dynamic and static code freely in the same file, and Godot won't complain. But adding type hints catches bugs at parse time, unlocks editor autocomplete, and makes your code measurably faster.
This page covers the full type system: from basic : int annotations to typed arrays, typed dictionaries (new in 4.4), casting with as, and the Variant type that sits underneath everything. If you need a refresher on script structure first, see foundations. For typed array iteration patterns, see arrays & loops.
GDScript has 38+ built-in types. Here are the ones you'll use most in 2D games:
| Category | Types |
|---|---|
| Primitives | bool, int, float |
| Strings | String, StringName, NodePath |
| Vectors | Vector2, Vector2i, Vector3, Vector3i |
| Engine | Color, Callable, Signal |
| Containers | Array, Dictionary |
| Packed arrays | PackedByteArray, PackedVector2Array, etc. (10 types) |
One thing that trips people up: most built-in types are pass-by-value. Vector2, Color, int, float all get copied on assignment or when passed to a function. Modifying a Vector2 inside a function does not change the original.
The exceptions are Object (and all subclasses like Node, Resource), Array, Dictionary, and packed arrays. These are pass-by-reference.
Three syntaxes cover everything:
# Explicit type
var health: int = 100
var player_name: String = "Hero"
var enemies: Array[Node2D] = []
# Type inference with :=
var speed := 50 # Inferred as int
var damage := 10.5 # Inferred as float
var label := "Hello" # Inferred as String
# Function signatures
func take_damage(amount: float) -> bool:
return health <= 0
For-loop variables can be typed too:
for name: String in names:
print(name)
for i: int in range(10):
print(i)
Constants work with both : and :=. There's no difference for constants since the value is known at compile time: const MAX_SPEED: float = 200.0 and const MAX_SPEED := 200.0 are identical.
Typed arrays landed in Godot 4.0. Add the element type in brackets:
var scores: Array[int] = [10, 20, 30]
var enemies: Array[Node] = [$Goblin, $Skeleton]
var items: Array[Item] = [Item.new()]
Godot checks the type on every write operation at runtime. Try to insert a wrong type and you get a runtime error. This also works with @export, giving you a smarter Inspector widget.
Typed dictionaries arrived in Godot 4.4 (March 2025). Same bracket syntax:
var fruit_costs: Dictionary[String, int] = {"apple": 5, "orange": 10}
var positions: Dictionary[Vector2i, Item] = {Vector2i(0, 0): Item.new()}
One limitation to know: nested typed collections are not supported. Array[Array[int]] won't compile. You can only go one level deep: Array[Array] works, but the inner array is untyped.
GDScript has two type operators: as for casting and is for checking.
Casting with as behaves differently depending on the type category:
# Object types: returns null on failure (safe)
var player := body as PlayerController
if not player:
return # Cast failed, body is not a PlayerController
# Built-in types: throws an ERROR on failure (dangerous!)
var num := value as int # Crashes if value is a String
This asymmetry is a common source of bugs. For Object types, as is a safe "try-cast." For built-in types like int or String, a failed cast crashes your game.
Checking with is is always safe:
if body is PlayerController:
body.damage() # Editor knows body is PlayerController here
if value is int:
print("It's an integer")
A useful pattern for node references:
@onready var timer := $Timer as Timer # Green line in editor
One gotcha: is type narrowing is not always recognized by the editor. After if event is InputEventMouseMotion:, you might still see UNSAFE_PROPERTY_ACCESS warnings when accessing event.relative. The workaround is to assign to a typed variable inside the block.
Yes. Typed GDScript generates optimized opcodes that bypass the Variant dispatch system. Instead of unwrapping a Variant, checking its type, looking up the operator, performing the operation, and wrapping the result, typed code takes a direct shortcut.
Independent benchmarks (1 billion iterations, M2 Max) show concrete gains:
| Operation | Speedup with types |
|---|---|
| Integer arithmetic | 28-36% faster |
| Vector2 distance calculation | 55-59% faster |
| Native function calls (fully typed) | Up to 150% faster |
The biggest wins come from vector math and engine API calls. If your movement code does position += velocity * delta every frame, typing those variables costs nothing and gives you a measurable speedup.
Variant is Godot's universal container type. It occupies 24 bytes on 64-bit platforms and can hold any of the 38+ built-in types. Every untyped variable in GDScript is a Variant.
var x = 5 # x is a Variant holding an int
var y: int = 5 # y is a native int, no Variant wrapper
Variant matters because many engine methods return it. Array.pop_back(), Dictionary.get(), signal parameters, and dynamic function arguments are all Variant. To get type safety back, you need to cast explicitly:
var text: String = arr.pop_back() # Implicit Variant -> String
var node := arr.pop_back() as Node # Explicit cast
One important nuance: only Object-typed variables can be null. Built-in value types (int, float, Vector2, etc.) always hold a valid value. There is no "null int" in GDScript.
Enums are named groups of integer constants:
enum State { IDLE, JUMP = 5, SHOOT }
# State.IDLE = 0, State.JUMP = 5, State.SHOOT = 6
Values auto-increment from the previous entry. You can use enums as type hints:
var current_state: State = State.IDLE
But enums are int under the hood. var x: State = 999 compiles without complaint, even if 999 is not a valid state. GDScript does not validate enum ranges at compile time. See exports tuning for exposing enums to the Inspector.
What You'll Practice: GDScript Types and Static Typing
Common GDScript Types and Static Typing Pitfalls
- `as` on built-in types (int, float, String) throws a runtime error instead of returning null, unlike Object types where it safely returns null
- Enums are int under the hood, so `var x: MyEnum = 999` compiles even if 999 is not a valid enum value
- Nested typed collections are not supported: `Array[Array[int]]` and `Dictionary[String, Dictionary[String, int]]` won't compile
- Reading @export values in `_init()` returns script defaults, not Inspector values. Read them in `_ready()` instead
- All Object-typed variables are nullable with no way to express non-null in the type system
GDScript Types and Static Typing FAQ
Is GDScript statically typed or dynamically typed?
GDScript is gradually typed. You can write fully dynamic code with no type annotations, fully static code with types everywhere, or any mix. The compiler enforces types where you add them and treats everything else as Variant.
Should I use type hints in GDScript?
Yes. Type hints catch errors at parse time instead of runtime, enable editor autocomplete, and generate faster bytecode. The cost is a few extra characters per declaration. The benefit is fewer bugs, better tooling, and measurable performance gains (28-59% depending on the operation).
What is the difference between String and StringName in GDScript?
String is a mutable sequence of Unicode characters. StringName is an immutable, interned string where only one instance of each unique value exists in memory. StringName comparisons are pointer-based (instant), making them faster for dictionary keys, signal names, and method lookups. Godot uses StringName internally for node names and methods.
Do typed arrays work with custom classes in GDScript?
Yes. Array[MyClass] works for any class registered with class_name. Godot checks the type at runtime on every write operation. If you try to insert an object of the wrong type, you get a runtime error.
When were typed dictionaries added to GDScript?
Godot 4.4, released March 2025. The syntax is Dictionary[KeyType, ValueType]. Like typed arrays, type checking happens at runtime on write operations.
What does := mean in GDScript?
:= is the type inference operator. var x := 5 infers the type as int from the right-hand side. It gives you the same type safety as var x: int = 5 with less typing. For constants, := and explicit typing are identical since the value is always known at compile time.
How much faster is typed GDScript?
Benchmarks show 28-36% faster integer arithmetic, 55-59% faster vector operations, and up to 150% faster native function calls when fully typed. The gains come from bypassing the Variant dispatch system with type-specific opcodes.
Can GDScript variables be null?
Only Object-typed variables (Node, Resource, and their subclasses) can be null. Built-in value types like int, float, Vector2, and String always hold a valid value. There is no "null int" or "null Vector2" in GDScript.
What is the Variant type in Godot?
Variant is Godot's universal container type. It is 24 bytes on 64-bit platforms and can hold any built-in type. Every untyped GDScript variable is a Variant. Many engine methods return Variant, which means you need explicit casts to get type safety back.
How do enums work in GDScript?
Enums define named integer constants: enum State { IDLE, RUN, JUMP }. Values auto-increment from 0. You can use them as type hints (var state: State), but GDScript does not validate enum ranges at compile time. An enum variable happily accepts any integer, even one outside the defined values.
GDScript Types and Static Typing Syntax Quick Reference
var health: int = 100
var speed := 50
var enemies: Array[Node2D] = []
var direction: Vector2 = Vector2.ZEROfunc take_damage(amount: float) -> bool:
health -= int(amount)
return health <= 0var scores: Array[int] = [10, 20, 30]
var loot: Dictionary[String, int] = {"gold": 50, "gems": 3}var player := body as PlayerController
if body is PlayerController:
body.take_hit(damage)enum State { IDLE, RUN, JUMP }
var current: State = State.IDLE