Skip to content
AgileLogics
6 min read

Optional Typing and Type Checking

Prerequisites: Classes and objects

Groovy is optionally typed: types are allowed everywhere but required nowhere. In dynamic code, types mostly document intent and convert values at runtime — the compiler doesn't check that methods exist.

String describe(List items) {
    "${items.size()} items"
}

println describe([1, 2, 3])

try {
    describe('not a list')
} catch (MissingMethodException e) {
    println 'runtime error: no describe(String)'
}
3 items
runtime error: no describe(String)

The bad call compiled fine and failed only when it ran. For scripts, that's often acceptable. For larger code bases, turn on type checking.

@TypeChecked

// illustrative — shows a compile error
import groovy.transform.TypeChecked

@TypeChecked
class Invoice {
    List<BigDecimal> lines = []

    BigDecimal total() {
        (BigDecimal) lines.sum()   // sum() is declared to return Object
    }

    String label() {
        "Invoice with ${lines.size()} lines, total ${totl()}"
    }
}
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
C:\demo\main.groovy: 13: [Static type checking] - Cannot find matching method Invoice#totl(). Please check if the declared type is correct and if the method exists.
 @ line 13, column 54.
           "Invoice with ${lines.size()} lines, total ${totl()}"
                                                        ^

1 error

@TypeChecked (on a class or method) makes the compiler verify method names, argument types, and return types — the typo totl() is now a compile error instead of a production bug.

Flow typing

Type checking is smart about def variables: it tracks the type a variable holds at each point.

import groovy.transform.TypeChecked

@TypeChecked
String shout(Object value) {
    def text = 'value: '
    if (value instanceof String) {
        return text + value.toUpperCase()   // value is known to be a String here
    }
    text + value
}

println shout('hi')
println shout(42)
value: HI
value: 42

After instanceof String, the checker knows value is a String and allows toUpperCase() without a cast.

Generics

import groovy.transform.TypeChecked

@TypeChecked
Map<String, Integer> wordLengths(List<String> words) {
    words.collectEntries { String w -> [w, w.size()] }
}

println wordLengths(['groovy', 'jvm'])
[groovy:6, jvm:3]

Generic types (List<String>) are checked at compile time with @TypeChecked and ignored at runtime otherwise, just as in Java.

When to use what

  • Scripts, build files, pipelines, tests — dynamic Groovy is fine; types where they help readability.
  • Libraries and application code — @TypeChecked or @CompileStatic (next lesson) on classes, so mistakes are caught early and IDEs can refactor safely.
  • DSL code and metaprogramming — stays dynamic, because it relies on runtime method resolution.