Static typing in GDScript

In this guide, you will learn:

  • how to use static typing in GDScript;

  • that static types can help you avoid bugs;

  • that static typing improves your experience with the editor.

Where and how you use this language feature is entirely up to you: you can use itonly in some sensitive GDScript files, use it everywhere, or don't use it at all.

Static types can be used on variables, constants, functions, parameters,and return types.

A brief look at static typing

With static typing, GDScript can detect more errors without even running the code.Also type hints give you and your teammates more information as you're working,as the arguments' types show up when you call a method. Static typing improveseditor autocompletion anddocumentationof your scripts.

Imagine you're programming an inventory system. You code anItem class,then anInventory. To add items to the inventory, the people who work withyour code should always pass anItem to theInventory.add() method.With types, you can enforce this:

class_nameInventoryfuncadd(reference:Item,amount:int=1):varitem:=find_item(reference)ifnotitem:item=_instance_item_from_db(reference)item.amount+=amount

Static types also give you better code completion options. Below, you can seethe difference between a dynamic and a static typed completion options.

You've probably encountered a lack of autocomplete suggestions after a dot:

Completion options for dynamic typed code.

This is due to dynamic code. Godot cannot know what value type you're passingto the function. If you write the type explicitly however, you will get allmethods, properties, constants, etc. from the value:

Completion options for static typed code.

Tip

If you prefer static typing, we recommend enabling theText Editor > Completion > Add Type Hints editor setting. Also considerenablingsome warnings that are disabled by default.

Also, typed GDScript improves performance by using optimized opcodes when operand/argumenttypes are known at compile time. More GDScript optimizations are planned in the future,such as JIT/AOT compilation.

Overall, typed programming gives you a more structured experience. Ithelps prevent errors and improves the self-documenting aspect of yourscripts. This is especially helpful when you're working in a team or ona long-term project: studies have shown that developers spend most oftheir time reading other people's code, or scripts they wrote in thepast and forgot about. The clearer and the more structured the code, thefaster it is to understand, the faster you can move forward.

How to use static typing

To define the type of a variable, parameter, or constant, write a colon after the name,followed by its type. E.g.varhealth:int. This forces the variable's typeto always stay the same:

vardamage:float=10.5constMOVE_SPEED:float=50.0funcsum(a:float=0.0,b:float=0.0)->float:returna+b

Godot will try to infer types if you write a colon, but you omit the type:

vardamage:=10.5constMOVE_SPEED:=50.0funcsum(a:=0.0,b:=0.0)->float:returna+b

Note

  1. There is no difference between= and:= for constants.

  2. You don't need to write type hints for constants, as Godot sets it automaticallyfrom the assigned value. But you can still do so to make the intent of your code clearer.Also, this is useful for typed arrays (likeconstA:Array[int]=[1,2,3]),since untyped arrays are used by default.

What can be a type hint

Here is a complete list of what can be used as a type hint:

  1. Variant. Any type. In most cases this is not much different from an untypeddeclaration, but increases readability. As a return type, forces the functionto explicitly return some value.

  2. (Only return type)void. Indicates that the function does not return any value.

  3. Built-in types.

  4. Native classes (Object,Node,Area2D,Camera2D, etc.).

  5. Global classes.

  6. Inner classes.

  7. Global, native and custom named enums. Note that an enum type is just anint,there is no guarantee that the value belongs to the set of enum values.

  8. Constants (including local ones) if they contain a preloaded class or enum.

You can use any class, including your custom classes, as types. There are two waysto use them in scripts. The first method is to preload the script you want to useas a type in a constant:

constRifle=preload("res://player/weapons/rifle.gd")varmy_rifle:Rifle

The second method is to use theclass_name keyword when you create.For the example above, yourrifle.gd would look like this:

class_nameRifleextendsNode2D

If you useclass_name, Godot registers theRifle type globally in the editor,and you can use it anywhere, without having to preload it into a constant:

varmy_rifle:Rifle

Specify the return type of a function with the arrow->

To define the return type of a function, write a dash and a right angle bracket->after its declaration, followed by the return type:

func_process(delta:float)->void:pass

The typevoid means the function does not return anything. You can use any type,as with variables:

funchit(damage:float)->bool:health_points-=damagereturnhealth_points<=0

You can also use your own classes as return types:

# Adds an item to the inventory and returns it.funcadd(reference:Item,amount:int)->Item:varitem:Item=find_item(reference)ifnotitem:item=ItemDatabase.get_instance(reference)item.amount+=amountreturnitem

Covariance and contravariance

When inheriting base class methods, you should follow theLiskov substitutionprinciple.

Covariance: When you inherit a method, you can specify a return type that ismore specific (subtype) than the parent method.

Contravariance: When you inherit a method, you can specify a parameter typethat is less specific (supertype) than the parent method.

Example:

class_nameParentfuncget_property(param:Label)->Node:# ...
class_nameChildextendsParent# `Control` is a supertype of `Label`.# `Node2D` is a subtype of `Node`.funcget_property(param:Control)->Node2D:# ...

Specify the element type of anArray

To define the type of anArray, enclose the type name in[].

An array's type applies tofor loop variables, as well as some operators like[],[...]= (assignment), and+. Array methods(such aspush_back) and other operators (such as==)are still untyped. Built-in types, native and custom classes,and enums may be used as element types. Nested array types (likeArray[Array[int]])are not supported.

varscores:Array[int]=[10,20,30]varvehicles:Array[Node]=[$Car,$Plane]varitems:Array[Item]=[Item.new()]vararray_of_arrays:Array[Array]=[[],[]]# var arrays: Array[Array[int]] -- disallowedforscoreinscores:# score has type `int`# The following would be errors:scores+=vehiclesvars:String=scores[0]scores[0]="lots"

Since Godot 4.2, you can also specify a type for the loop variable in afor loop.For instance, you can write:

varnames=["John","Marta","Samantha","Jimmy"]forname:Stringinnames:pass

The array will remain untyped, but thename variable within thefor loopwill always be ofString type.

Specify the element type of aDictionary

To define the type of aDictionary's keys and values, enclose the type name in[]and separate the key and value type with a comma.

A dictionary's value type applies tofor loop variables, as well as some operators like[] and[...]= (assignment). Dictionary methods that return valuesand other operators (such as==) are still untyped. Built-in types, nativeand custom classes, and enums may be used as element types. Nested typed collections(likeDictionary[String,Dictionary[String,int]]) are not supported.

varfruit_costs:Dictionary[String,int]={"apple":5,"orange":10}varvehicles:Dictionary[String,Node]={"car":$Car,"plane":$Plane}varitem_tiles:Dictionary[Vector2i,Item]={Vector2i(0,0):Item.new(),Vector2i(0,1):Item.new()}vardictionary_of_dictionaries:Dictionary[String,Dictionary]={{}}# var dicts: Dictionary[String, Dictionary[String, int]] -- disallowedforcostinfruit_costs:# cost has type `int`# The following would be errors:fruit_costs["pear"]+=vehiclesvars:String=fruit_costs["apple"]fruit_costs["orange"]="lots"

Type casting

Type casting is an important concept in typed languages.Casting is the conversion of a value from one type to another.

Imagine anEnemy in your game, thatextendsArea2D. You want it to collidewith thePlayer, aCharacterBody2D with a script calledPlayerControllerattached to it. You use thebody_entered signal to detect the collision.With typed code, the body you detect is going to be a genericPhysicsBody2D,and not yourPlayerController on the_on_body_entered callback.

You can check if thisPhysicsBody2D is yourPlayer with theas keyword,and using the colon: again to force the variable to use this type.This forces the variable to stick to thePlayerController type:

func_on_body_entered(body:PhysicsBody2D)->void:varplayer:=bodyasPlayerControllerifnotplayer:returnplayer.damage()

As we're dealing with a custom type, if thebody doesn't extendPlayerController, theplayer variable will be set tonull.We can use this to check if the body is the player or not. We will alsoget full autocompletion on the player variable thanks to that cast.

Note

Theas keyword silently casts the variable tonull in case of a typemismatch at runtime, without an error/warning. While this may be convenientin some cases, it can also lead to bugs. Use theas keyword only if thisbehavior is intended. A safer alternative is to use theis keyword:

ifnot(bodyisPlayerController):push_error("Bug: body is not PlayerController.")varplayer:PlayerController=bodyifnotplayer:returnplayer.damage()

You can also simplify the code by using theisnot operator:

ifbodyisnotPlayerController:push_error("Bug: body is not PlayerController")

Alternatively, you can use theassert() statement:

assert(bodyisPlayerController,"Bug: body is not PlayerController.")varplayer:PlayerController=bodyifnotplayer:returnplayer.damage()

Note

If you try to cast with a built-in type and it fails, Godot will throw an error.

Safe lines

You can also use casting to ensure safe lines. Safe lines are a tool to tell youwhen ambiguous lines of code are type-safe. As you can mix and match typedand dynamic code, at times, Godot doesn't have enough information to know ifan instruction will trigger an error or not at runtime.

This happens when you get a child node. Let's take a timer for example:with dynamic code, you can get the node with$Timer. GDScript supportsduck-typing,so even if your timer is of typeTimer, it is also aNode andanObject, two classes it extends. With dynamic GDScript, you also don'tcare about the node's type as long as it has the methods you need to call.

You can use casting to tell Godot the type you expect when you get a node:($TimerasTimer),($PlayerasCharacterBody2D), etc.Godot will ensure the type works and if so, the line number will turngreen at the left of the script editor.

Unsafe vs Safe Line

Unsafe line (line 7) vs Safe Lines (line 6 and 8)

Note

Safe lines do not always mean better or more reliable code. See the note aboveabout theas keyword. For example:

@onreadyvarnode_1:=$Node1asType1# Safe line.@onreadyvarnode_2:Type2=$Node2# Unsafe line.

Even thoughnode_2 declaration is marked as an unsafe line, it is morereliable thannode_1 declaration. Because if you change the node typein the scene and accidentally forget to change it in the script, the errorwill be detected immediately when the scene is loaded. Unlikenode_1,which will be silently cast tonull and the error will be detected later.

Note

You can turn off safe lines or change their color in the editor settings.

Typed or dynamic: stick to one style

Typed GDScript and dynamic GDScript can coexist in the same project. Butit's recommended to stick to either style for consistency in your codebase,and for your peers. It's easier for everyone to work together if you followthe same guidelines, and faster to read and understand other people's code.

Typed code takes a little more writing, but you get the benefits we discussedabove. Here's an example of the same, empty script, in a dynamic style:

extendsNodefunc_ready():passfunc_process(delta):pass

And with static typing:

extendsNodefunc_ready()->void:passfunc_process(delta:float)->void:pass

As you can see, you can also use types with the engine's virtual methods.Signal callbacks, like any methods, can also use types. Here's abody_enteredsignal in a dynamic style:

func_on_area_2d_body_entered(body):pass

And the same callback, with type hints:

::
func _on_area_2d_body_entered(body: PhysicsBody2D) -> void:

pass

Warning system

Note

Detailed documentation about the GDScript warning system has been moved toGDScript warning system.

Godot gives you warnings about your code as you write it. The engine identifiessections of your code that may lead to issues at runtime, but lets you decidewhether or not you want to leave the code as it is.

We have a number of warnings aimed specifically at users of typed GDScript.By default, these warnings are disabled, you can enable them in Project Settings(Debug > GDScript, make sureAdvanced Settings is enabled).

You can enable theUNTYPED_DECLARATION warning if you want to always usestatic types. Additionally, you can enable theINFERRED_DECLARATION warningif you prefer a more readable and reliable, but more verbose syntax.

UNSAFE_* warnings make unsafe operations more noticeable, than unsafe lines.Currently,UNSAFE_* warnings do not cover all cases that unsafe lines cover.

Common unsafe operations and their safe counterparts

Global scope methods

The following global scope methods are not statically typed, but they havetyped counterparts available. These methods return statically typed values:

Method

Statically typed equivalents

abs()

ceil()

clamp()

floor()

lerp()

round()

sign()

snapped()

When using static typing, use the typed global scope methods whenever possible.This ensures you have safe lines and benefit from typed instructions forbetter performance.

UNSAFE_PROPERTY_ACCESS andUNSAFE_METHOD_ACCESS warnings

In this example, we aim to set a property and call a method on an objectthat has a script attached withclass_nameMyScript and thatextendsNode2D. If we have a reference to the object as aNode2D (for instance,as it was passed to us by the physics system), we can first check if theproperty and method exist and then set and call them if they do:

if"some_property"innode_2d:node_2d.some_property=20# Produces UNSAFE_PROPERTY_ACCESS warning.ifnode_2d.has_method("some_function"):node_2d.some_function()# Produces UNSAFE_METHOD_ACCESS warning.

However, this code will produceUNSAFE_PROPERTY_ACCESS andUNSAFE_METHOD_ACCESS warnings as the property and method are not presentin the referenced type - in this case aNode2D. To make these operationssafe, you can first check if the object is of typeMyScript using theis keyword and then declare a variable with the typeMyScript onwhich you can set its properties and call its methods:

ifnode_2disMyScript:varmy_script:MyScript=node_2dmy_script.some_property=20my_script.some_function()

Alternatively, you can declare a variable and use theas operator to tryto cast the object. You'll then want to check whether the cast was successfulby confirming that the variable was assigned:

varmy_script:=node_2dasMyScriptifmy_script!=null:my_script.some_property=20my_script.some_function()

UNSAFE_CAST warning

In this example, we would like the label connected to an object entering ourcollision area to show the area's name. Once the object enters the collisionarea, the physics system sends a signal with aNode2D object, and the moststraightforward (but not statically typed) solution to do what we want couldbe achieved like this:

func_on_body_entered(body:Node2D)->void:body.label.text=name# Produces UNSAFE_PROPERTY_ACCESS warning.

This piece of code produces anUNSAFE_PROPERTY_ACCESS warning becauselabel is not defined inNode2D. To solve this, we could first check if thelabel property exist and cast it to typeLabel before settings its textproperty like so:

func_on_body_entered(body:Node2D)->void:if"label"inbody:(body.labelasLabel).text=name# Produces UNSAFE_CAST warning.

However, this produces anUNSAFE_CAST warning becausebody.label is of aVariant type. To safely get the property in the type you want, you can use theObject.get() method which returns the object as aVariant value or returnsnull if the property doesn't exist. You can then determine whether theproperty contains an object of the right type using theis keyword, andfinally declare a statically typed variable with the object:

func_on_body_entered(body:Node2D)->void:varlabel_variant:Variant=body.get("label")iflabel_variantisLabel:varlabel:Label=label_variantlabel.text=name

Cases where you can't specify types

To wrap up this introduction, let's mention cases where you can't use type hints.This will trigger asyntax error.

  1. You can't specify the type of individual elements in an array or a dictionary:

varenemies:Array=[$Goblin:Enemy,$Zombie:Enemy]varcharacter:Dictionary={name:String="Richard",money:int=1000,inventory:Inventory=$Inventory,}
  1. Nested types are not currently supported:

varteams:Array[Array[Character]]=[]

Summary

Typed GDScript is a powerful tool. It helps you write more structured code,avoid common errors, and create scalable and reliable systems. Static typesimprove GDScript performance and more optimizations are planned for the future.


User-contributed notes

Please read theUser-contributed notes policy before submitting a comment.