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 —
@TypeCheckedor@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.