Groovy Type Checking and Static Compilation
last modified October 15, 2026
Groovy is dynamically typed by default. A method call on an object is resolved
when the code runs, so a misspelled method name is found only when that line
executes. For larger programs, that can be too late. Groovy offers two
annotations that move these checks to compile time: @TypeChecked
checks types while keeping the dynamic runtime, and @CompileStatic
compiles the code the way Java does. In this tutorial, we use both, and show
how to opt out for parts of the code that need to stay dynamic.
Errors Found at Runtime by Default
Without any annotation, Groovy does not check the method name
lenght until the call runs. The program prints the first line and
fails on the second.
def s = 'hello' println "before" println s.lenght() println "after"
The error is only found because this line runs. A rarely used branch with the same mistake would stay hidden until a user reaches it. Type checking lets the compiler find such problems before the script starts.
$ groovy Dynamic.groovy before Caught: groovy.lang.MissingMethodException: No signature of method: lenght for class: java.lang.String is applicable for argument types: () values: [] ...
@TypeChecked
The @TypeChecked annotation, from groovy.transform,
applies static type checking to a class, method, or constructor. The compiler
checks assignments, method calls, and return types inside the annotated code.
The code still uses Groovy's dynamic dispatch at runtime, so it behaves the
same way as before.
import groovy.transform.TypeChecked
@TypeChecked
void run() {
String s = 'hello'
println s.lenght()
}
run()
The method lenght does not exist on String, so the
compiler reports an error and nothing runs. The message names the file, the
line, and the column.
$ groovy TypeCheckedError.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
TypeCheckedError.groovy: 6: [Static type checking] - Cannot find matching method java.lang.String#lenght(). Please check if the declared type is correct and if the method exists.
@ line 6, column 13.
println s.lenght()
^
1 error
Assignment Checks
Assignments are checked too. Assigning a string to an int variable
is rejected at compile time:
import groovy.transform.TypeChecked
@TypeChecked
void run() {
int count = 'ten'
println count
}
run()
$ groovy AssignmentError.groovy
AssignmentError.groovy: 5: [Static type checking] - Cannot assign value of type java.lang.String to variable of type int
@ line 5, column 17.
int count = 'ten'
^
Valid Typed Code
Code that passes the checks compiles and runs as usual. Here the method uses typed parameters, a typed local variable, and a loop:
import groovy.transform.TypeChecked
@TypeChecked
int sumTo(int n) {
int total = 0
for (int i = 1; i <= n; i++) {
total += i
}
total
}
println sumTo(10)
$ groovy TypeCheckedOk.groovy 55
Flow Typing
A variable declared with def has no declared type, but
@TypeChecked remembers the type assigned most recently. After
x = 42, the compiler knows that x is an
Integer, and it rejects a call that only String
has.
import groovy.transform.TypeChecked
@TypeChecked
void run() {
def x = 'abc'
x = 42
println x.toUpperCase()
}
run()
$ groovy FlowTyping.groovy
FlowTyping.groovy: 7: [Static type checking] - Cannot find matching method java.lang.Integer#toUpperCase(). Please check if the declared type is correct and if the method exists.
@ line 7, column 13.
println x.toUpperCase()
^
@CompileStatic
@CompileStatic goes further than @TypeChecked. It
checks the code in the same way, and it also compiles method calls to direct
calls on known types, instead of looking them up at runtime. This makes the
code closer to Java in performance and in its failure modes. Use it for code
that is performance-sensitive or that is part of a stable API.
import groovy.transform.CompileStatic
@CompileStatic
class Calc {
int add(int a, int b) { a + b }
String describe(int x) { "value=${x}" }
}
def c = new Calc()
println c.add(2, 3)
println c.describe(7)
The annotation on a class applies to all its methods. The script's top-level code, outside any class or method, is not compiled statically, so the checks are applied to methods instead.
$ groovy StaticCalc.groovy 5 value=7
Property Access Checks
A bad property access placed inside a method marked with
@CompileStatic is also reported at compile time.
import groovy.transform.CompileStatic
class Person {
String name
}
@CompileStatic
void show() {
def p = new Person(name: 'Ann')
println p.nmae
}
show()
The typo in nmae is reported at compile time, with the class name
and the line number. Without @CompileStatic, the same program would
run until that line and then fail at runtime.
$ groovy StaticError.groovy
StaticError.groovy: 10: [Static type checking] - No such property: nmae for class: Person
@ line 10, column 13.
println p.nmae
^
Dynamic Code with @CompileDynamic
Some code needs runtime flexibility, such as reading properties from a map or
an untyped object. The @CompileDynamic annotation turns off static
checking for one method inside an otherwise static class.
import groovy.transform.CompileStatic
import groovy.transform.CompileDynamic
@CompileStatic
class Report {
String title(Object o) { o.toString().toUpperCase() }
@CompileDynamic
String dyn(Object o) { o.name ?: 'no name' }
}
def r = new Report()
println r.title('abc')
println r.dyn([name: 'Zed'])
title is statically compiled. dyn is not, so
o.name is resolved at runtime, which allows a Map
argument whose key looks like a property.
The same class can mix typed and untyped methods. Typed methods are checked
and fast, and only the methods that truly need dynamic behavior are annotated
with @CompileDynamic.
$ groovy MixedDynamic.groovy ABC Zed
Comparing the Modes
| Mode | Checks at compile time | Runtime behavior | Typical use |
|---|---|---|---|
| Default (none) | Syntax only | Dynamic dispatch | Scripts and quick tools |
@TypeChecked | Types, method existence, assignments | Dynamic dispatch | Adding safety without changing behavior |
@CompileStatic | Same as @TypeChecked, plus static resolution | Direct, Java-like calls | Stable APIs and hot code paths |
@CompileDynamic | Nothing for that method | Dynamic dispatch | Code that needs runtime lookups |
Best Practices
- Start with
@TypeChecked. It catches many mistakes without changing how the code runs. - Use
@CompileStaticfor stable code. Apply it to classes whose types are well defined, such as domain objects and libraries. - Keep dynamic parts small. Mark only the methods that need
runtime lookups with
@CompileDynamic. - Type the public API. Declared parameter and return types make the checks useful for callers too.
- Do not expect every check everywhere. Static checking
depends on the types the compiler can see. Calls into untyped
defvalues and some dynamic Groovy features are not fully checked.
Source
This tutorial showed how Groovy checks code by default, how @TypeChecked
and @CompileStatic move errors from runtime to compile time, and how
@CompileDynamic opts a method out of static checking.
Author
List all Groovy tutorials.