ZetCode

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.

Dynamic.groovy
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.

TypeCheckedError.groovy
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:

AssignmentError.groovy
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:

TypeCheckedOk.groovy
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.

FlowTyping.groovy
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.

StaticCalc.groovy
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.

StaticError.groovy
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.

MixedDynamic.groovy
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

ModeChecks at compile timeRuntime behaviorTypical use
Default (none)Syntax onlyDynamic dispatchScripts and quick tools
@TypeCheckedTypes, method existence, assignmentsDynamic dispatchAdding safety without changing behavior
@CompileStaticSame as @TypeChecked, plus static resolutionDirect, Java-like callsStable APIs and hot code paths
@CompileDynamicNothing for that methodDynamic dispatchCode that needs runtime lookups

Best Practices

Source

Groovy documentation

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than eight years of experience in teaching programming.

List all Groovy tutorials.