Elixir v1.20 Release: Introduction of Gradual Typing
Elixir v1.20 transforms the language into a gradually typed language by implementing a type system capable of performing type inference and checking every program for "verified bugs"—typing violations guaranteed to fail at runtime—without requiring any initial type annotations from the developer.
A Set-Theoretic Approach to Gradual Typing
Elixir's new type system is designed to be sound, gradual, and developer-friendly. It utilizes set-theoretic types, meaning types are composed using basic set operations: unions, intersections, and negations.
To avoid the friction of introducing type annotations into an existing ecosystem, the first development milestone focuses on providing value through inference. The system identifies dead code and verified bugs efficiently with a low false-positive rate, allowing developers to find errors in existing codebases simply by upgrading the compiler.
The dynamic() Type: Compatibility and Narrowing
Unlike the any() type found in many gradual type systems—which often suppresses all type violations—Elixir introduces the dynamic() type. This type is characterized by two primary properties:
Compatibility
Elixir only emits a typing violation when the supplied types and the accepted types are completely disjoint. If there is any overlap (compatibility), the compiler allows the code to proceed. This prevents the system from rejecting valid programs that a strict static type system might flag as false positives.
Narrowing
The dynamic() type acts as a range that is refined as the program executes. For example, if a variable is used as a map key (data.a), Elixir narrows the type of that variable to a map containing that specific key. If the variable is subsequently used in a way that contradicts this narrowed type (e.g., treating a map as a number), the compiler reports a violation.
Type Inference in Guards and Clauses
Elixir v1.20 extends type checking and narrowing to several core language constructs:
- Guards: The system can infer unions, intersections, and negations from guards. For instance,
is_list(x) and is_integer(y)correctly infers the types ofxandy. It can also handle negations, such asnot is_map_key(x, :foo), which flags subsequent attempts to accessx.fooas violations. - Size Assertions: Expressions like
tuple_size(x) < 3allow the compiler to track that a tuple has at most two elements, flaggingelem(x, 3)as a bug. - Case and Conditionals: The compiler uses information from previous clauses to refine subsequent ones. If a
casestatement matchesnilin the first clause, the compiler knows the variable in the second clause cannot benil, allowing for more precise type checking.
Compilation and Performance Improvements
Beyond the type system, Elixir v1.20 introduces significant performance gains:
- Build Speed: Synthetic benchmarks indicate that Elixir's build tool is now the fastest among BEAM languages, particularly on multi-core machines.
- Module Definition Option: A new
:module_definitioncompiler option allows developers to choose between:compiled(default) and:interpretedexecution for the contents ofdefmodule. Settingelixirc_options: [module_definition: :interpreted]inmix.exscan improve compilation times for large projects.
Future Roadmap
Full type signatures are not yet available. The Elixir team will introduce them only after achieving specific milestones:
- Satisfactory performance of the v1.20 type system.
- Efficient implementation of recursive types.
- Efficient implementation of parametric types.
- Efficient implementation of traversing map key-value pairs as an enumerable.
Community Perspective
The release has been met with significant enthusiasm, particularly from developers who previously avoided Elixir due to the lack of static types.
"It's very nice updating Elixir, having no breaking changes across my many projects and it then the compiler just finds bugs for free."
Some community members have noted the strategic advantage of set-theoretic types over the systems used in TypeScript or Scala, specifically the inclusion of complements (negations), which allows for functional completeness in type expression. Others have compared the experience to Gleam, noting that while Gleam provides types "upfront," Elixir's approach allows it to maintain its existing ecosystem (including Phoenix and Ecto) while gradually adding safety.