Migrating a Dynamic Racket Codebase to Typed Racket: A Practical Guide
Add static types to a dynamic Racket project step‑by‑step. Learn how Typed Racket’s gradual typing, the Any type, and unchecked modules let you migrate without rewriting everything at once.
17 Aug 2026, 06:31 UTC

Problem: A Dynamic Racket Codebase Needs Reliability
Many Racket projects start as pure dynamic code. As they grow, bugs slip in, and the lack of compile‑time guarantees hurts confidence. Developers often ask: Can we get static type safety without tearing the code apart?
Thesis: Typed Racket Provides a Pragmatic Path
Typed Racket extends the base language with optional type annotations, a type checker, and a set of features that let you keep dynamic flexibility while gaining static safety. The key is gradual typing: you can annotate parts of the code, leave others untyped, and the system will bridge them with the Any type.
1. Understanding Gradual Typing in Typed Racket
- Optional annotations – you add
#:typedeclarations to functions or variables. If omitted, the checker treats the expression asAny. - Type inference – for unannotated functions, Typed Racket can infer the most general type it can, reducing annotation overhead.
- Polymorphism – you can write generic functions with type variables, e.g.,
(All (α) (α -> α)). - Contracts – you can enforce runtime checks for values that escape the type system.
2. Incremental Migration Techniques
-
Wrap legacy modules with
unchecked. A module marked(module+ unchecked)suppresses type errors inside it, allowing you to keep the old code while the rest of the project checks against it.(module my-lib racket (require typed/racket) (provide (all-defined-out)) (module+ unchecked (define (legacy-func x) (displayln x)))) -
Use the
Anytype as a bridge. When a typed function receives an untyped value, annotate the parameter asAny. This keeps the checker happy but signals that you should verify the value at runtime if needed.(define (print-any x : Any) (displayln x)) -
Gradual annotation of public API. Start by typing the functions that external callers use. Internal helpers can stay dynamic until you’re ready to type them.
-
Run
typed/racket-checkfrequently. This command analyzes the whole project and reports type errors per module. It’s a fast feedback loop for incremental changes.typed/racket-check path/to/project
3. Worked Example – Adding Types to a Simple Calculator
Suppose we have a dynamic module that adds two numbers and formats the result.
;; dynamic-calc.rkt
(define (add a b)
(+ a b))
(define (format-result x)
(format "Result: ~a" x))
(define (run a b)
(format-result (add a b)))
We want to type add and format-result but keep run dynamic for now.
;; typed-calc.rkt
#lang typed/racket
(provide run)
(define (add : (Integer Integer -> Integer) a b)
(+ a b))
(define (format-result : (Integer -> String) x)
(format "Result: ~a" x))
(module+ unchecked
(define (run a b)
(format-result (add a b)))
Running typed/racket-check on this file will show no errors. The unchecked module lets run call typed functions without the checker verifying its arguments. If later you decide to type run, you simply remove the unchecked wrapper and add a signature.
Trade‑offs and Limitations
- Overly general inferred types: The checker may infer
Anyor very broad types, masking bugs until you add explicit annotations. - Migrating large codebases: A single run of
typed/racket-checkmay surface dozens of errors, requiring careful planning to fix them in stages. - Runtime overhead: Some Typed Racket features, like contracts, add runtime checks that can affect performance.
Actionable Closing
Start by annotating the public API of your library, wrap legacy modules with unchecked, and run typed/racket-check after each change. Keep the Any type as a temporary bridge, and gradually replace it with precise types. Over time, the type system will surface hidden bugs and give you confidence that your Racket code behaves as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.