Type-Safety

Hey guys, welcome to the first bonus episode of our action/adventure series! We’re talking about type-safety and the benefits of using type annotations in our Godot project.
Like the previous episode around coding, this is a topic that can be a bit dull… but it’s an extremely useful (and important) concept to understand and learning about it now can really help you in the long run as your projects get more complex.
What is “Type-Safety”?
Type-safety is a programming concept that ensures that variables and functions are used in a way that is consistent with their intended usage. Or the short version: it lets us catch an entire set of bugs and errors before they even happen.
Whether you see it written out or not, all values have a type. For example, the value of 5.0
is a float
, the value of "hello"
is a string
, and the value of true
is a boolean
…
Heck, we talked about classes last episode - well, classes are also types! An instance of a class has a type that is the class used to create the instance. That’s why you’ll hear us say things like a Vector3
with a value of Vector3(1, 2, 3)
— Vector3
is a type and Vector3(1, 2, 3)
is the usage of that type.
Each type has its own set of rules and behaviors that determine where and how it can be used. For example, you can’t add a string
and an int
together, because the rules of the string
type don’t allow for that kind of operation. This is where type-safety comes in — it ensures that we only use values in ways that are consistent with their types.
There are two types of languages
Programming languages can be broadly categorized into 2 categories: statically typed and dynamically typed. You’ll also hear these referred to as strongly typed and weakly typed1.
Statically Typed Languages
In a statically typed language, the type of a variable is known at “compile time”2 and cannot change. This allows the compile to catch “type errors” before the code is even run. For example, if you try to assign a string
value to a variable that is declared as an int
, the compiler will throw an error.
In GDScript, we can declare the type of a variable using a colon :
followed by the type name. For example:
# Here is a variable declaration in GDScript that
# holds an integer (whole number).
var my_number: int = 5
# And now a function declaration that takes two
# integers and returns an integer.
func add(a: int, b: int) -> int:
return a + b
Many programming languages, especially those in the game development space, are statically typed. The most notable examples would likely be C++ and C#. These languages in particular place their types at the beginning of a variable or function declaration, rather than a keyword. Which admittedly leads to some intimidating looking code for beginners.
Let’s recreate our GDScript example from above in C++ for comparison:
// Here is a variable declaration in C++ that
// holds an integer (whole number).
int my_number = 5;
// And now a function declaration that takes two
// integers and returns an integer.
int add(int a, int b) {
return a + b;
}
Optimization Potential
This explicit declaration of types allows the compiler to optimize the code better, as it can make assumptions safely about the types of values and how they will be used. This can lead to better performance and reduced memory usage.
For example, if you declare a variable as an
int
, then the compiler knows it can reserve a fixed amount of memory3 for that variable and can optimize the code accordingly. In contrast, if you declare a variable as astring
, the compiler may need to allocate memory dynamically and may even layout the memory differently depending on the length of thestring
.
Dynamically Typed Languages
On the other hand, in a dynamically typed language, the type of a variable is determined at runtime based on the value assigned to it. This means that you can assign a string
value to a variable and then later assign an int
value to the same variable without any errors. This can lead to more flexible code, but it also means that type errors may not be caught until the code is actually run.
If we were to apply this to our “variables are boxes” analogy from the previous episode, it’s like a giant box that can hold anything. Until you look at what’s currently in the box, you have no idea what it is. With type-safety, it’s like giving the box a very specific shape (or label) that only allows certain things to fit inside. This way, you know exactly what you’re dealing with at all times.
How is this possible?
Dynamically typed languages are usually found in places where the code is not being executed at the “native” or “machine” level4. Instead, the “code” is being executed within a special environment, often called a “virtual machine”5 or “VM”, that is able to represent values in a more flexible way.
Each value in a virtual machine is stored in a special format that includes information about its type. When the value is used, this type information is used to determine how the VM should handle it. This allows for more flexibility, but it also means that the VM has to do more work at runtime to figure out what to do with each value.
In Godot, this special type is exposed directly to us and is called a
Variant
. AVariant
is a special type that can hold any value, and when we don’t specify a type, GDScript will automatically useVariant
as the type.
Opting into type-safety
We saw that in GDScript, we can declare the type of a variable using a colon :
followed by the type name. But, we can also leave it off entirely and GDScript will let us assume the type could be anything. This is because GDScript is actually a dynamically typed language.
So why can we also use it like a statically typed language? Well, GDScript (like many other dynamically typed languages these days) allows for “optional type annotations”. This means that you can choose to specify the type of something like a variable or function parameter, like a statically typed language, but you don’t have to.
When we use type annotations, we get the benefits of type-safety, and some performance improvements, as if the language was statically typed. The biggest benefit obviously being that we can catch type errors before they even happen.
Most languages that support typing won’t even let you finish compiling or run your code until all the issues have been fixed. Best of all is these errors are often told to us directly in our code editor as we’re writing the line of offending code.

Here we tried to assign an int
to a variable that is declared as a String
. The editor is telling us that this is an error and we need to fix it.
This results in you spending less time bouncing between your code and trying to run the game to see if it works6. Or worse yet, imagine if a value changed to some unexpected type in the middle of a huge function call chain. These are the types of bugs that can eat literal hours of your time, only to fix it in 0.2 seconds once you find it.
Forcing type-safety using Godot’s settings
So for moving forward in our series, we want to make sure that we’re always using type annotations. This will prepare you for working, not just with GDScript, but also other languages that are statically typed (like C++ or C#).

To do this we’re going to open up our project settings, enable Advanced Settings
, and then search for either gdscript
or untyped declarations
to find the setting we’re looking for.
There’s a large number of settings in here you can use to really customize Godot’s behavior around GDScript and types, but the only one we care about right now is Untyped Declarations
. Our recommendation is to set it to Error
, since this will force us to use type annotations when declaring things like variables.
Shoutout to BearLikeLion
We learned about this setting from Discord member, bearlikelion and it’s a game changer! Help us show him some love by checking out his upcoming game, Surfs Up, which is currently in development and open to playtesting.
Type inference
It may feel a bit redundant to have to specify the type of a variable if the value we’re assigning to it, if that value is clearly a specific type of thing. For example, if we have a variable that is assigned a value of 5
, we know that the type is an int
. So why do we need to specify it?
A modern practice in languages that supports types is a feature called type inference. This allows the computer to try and determine the type of a variable based on the initial value assigned to it when it’s first declared. In GDScript, we do this by prefixing the assignment operator with a colon7.
# This is your standard assignment operator
var my_number = 5
# This is the type inference operator
var my_number := 5
# ... which is equivalent to
var my_number: int = 5
The only requirement is that the computer has to be able to determine the type of the value on the right side of the assignment operator. Values like literals (like 5
, "hello"
, or true
) are easy to figure out, but if value is another variable that isn’t typed, then this becomes a trickier problem.

It’s nearly impossible for the computer to figure what the value of this
is, since it can be anything. There’s no way to determine the type of that
.
In this case, the computer will throw an error, rather than propagate the issue by assuming the type is Variant
(which is how Godot handles untyped variables). By using the :=
operator, we’ll telling Godot we don’t want it to make assumptions about the type of the variable.
Constants are always known
One weird thing is that this rule doesn’t apply to constants. Why not?
Well this is because constants are always known ahead of time. We can only use certain types of expressions with constants — most of which are going to be literals, which are trivial for the computer to infer.
We can also only set the constant once while declaring it, meaning it can never change. So there’s never ambiguity about what the type may be at any point in the future.
Functions need type annotations too
In GDScript, we can also specify the type of a function’s parameters and return value. If you think of parameters like variables without the var
keyword, then it’s easy to see how this works. But in order to open a function body, we need to use a colon… so how do we specify the return type?
This is where the ->
operator comes in. The name of the type to the right of this will be the expected return type of the function. This means whatever value we return
from the function must be of this type.
# This is a function that takes two integers and returns an integer
func add(a: int, b: int) -> int:
return a + b
But what if your function doesn’t return anything? Well, in that case, we can use the void
type. This is a unique (and somewhat confusing) type that is used to indicate that a function does not return anything. That’s not to say the function can’t use return
statements to end the function early — it just won’t give a value back to the caller.
# This function prints a message based on the values of a and b
# and returns nothing (void)
func is_greater_than(a: int, b: int) -> void:
if a > b:
print("a is greater than b")
return # Exit the function early
print("b is greater than a")
Unlike other types, like int
or string
, there is no value associated with the type void
. This means we can’t use it to declare a variable or assign a value to it. And if you’re wondering whether void
is somehow related to null
, the answer is no. null
is a special value that represents the absence of a value, while void
is a type that represents the absence of… anything8.
Another example of type-safety
Since dynamically typed languages have to be flexible, when a property or method is accessed on a variable it knows nothing about, it has no way to determine if the member you’re accessing exists or not. Doing this would result in a runtime error when the code is executed.
This becomes an easy source of bugs that can be tricky to track down. Something like a typo in a member name can be hard to spot, since it looks correct until you check each character in the name. Take this example, where we’re trying to use the normalized()
method on a Vector3
variable:
var my_vector = Vector3(1, 2, 3)
var normalized_vector = my_vector.normalize() # Oops! This is a typo
All we did was forget the d
at the very end. Another possible issue is that you could have a function called normalize()
that does something completely different or doesn’t return a value at all9. Without type-safety, this would look correct but result in a really confusing bug when the code is run since it’s almost correct.
That’s about it!
We hope you enjoyed this little detour into type-safety and GDScript’s optional type annotations. Hopefully it’s given you a better understanding of how GDScript works and why type-safety is important.
Episode 3 is on the way and we’ll finally be returning to the 3D scene we started in episode 1 as we make our game feel more like a game.