{"slug": "elixir-v1-20-released-now-a-gradually-typed-language", "title": "Elixir v1.20 released: now a gradually typed language", "summary": "Elixir v1.20 has been released, introducing a gradually typed system that performs type inference and type checking on all Elixir programs without requiring type annotations. The update enables Elixir to detect dead code and verified bugs — typing violations guaranteed to fail at runtime — with a low false positive rate, passing 12 of 13 categories in the \"If T: Benchmark for Type Narrowing.\" The milestone, supported by CNRS, Remote, Fresha, and Tidewave, brings set-theoretic types to the language through a partnership that began with research in 2022.", "body_md": "# Elixir v1.20 released: now a gradually typed language\n\nIn 2022, [we announced the effort to add set-theoretic types to Elixir](/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/). In June 2023, we [published an award winning paper on Elixir’s type system design](https://arxiv.org/abs/2306.06391) and said our work was transitioning [from research to development](/blog/2023/06/22/type-system-updates-research-dev/).\n\nWith Elixir v1.20, we have completed our first development milestone which is to perform type inference and gradually type check every Elixir program, without introducing type annotations. This means Elixir increasingly reports dead code and *verified bugs*: typing violations that are guaranteed to fail at runtime if executed. Elixir can find verified bugs in existing programs efficiently, without introducing developer overhead, and with an extremely low false positives rate.\n\nIn this announcement, we will break down the type system goals, what the `dynamic()`\n\ntype means in Elixir, and how it finds *verified bugs*. In particular, our implementation performs well in the [“If T: Benchmark for Type Narrowing”](https://github.com/utahplt/ifT-benchmark/tree/main#benchmark-results) benchmark. Elixir passes 12 of the 13 categories, showing that it can recover precise type information from ordinary Elixir code, which we use to find verified bugs in dynamically typed programs.\n\nThe type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), and [Tidewave](https://tidewave.ai/).\n\n## Types, in my Elixir?\n\nOur goal is to introduce a type system which is:\n\n-\n**sound**- the types inferred and assigned by the type system align with the behaviour of the program -\n**gradual**- Elixir’s type system includes the`dynamic()`\n\ntype, which can be used when the type of a variable or expression is checked at runtime. In the absence of`dynamic()`\n\n, Elixir’s type system behaves as a static one -\n**developer friendly**- the types are described, implemented, and composed using basic set operations: unions, intersections, and negations (hence it is a set-theoretic type system), with clear error messages\n\nIntroducing a type system into an existing language is a complex change. For this reason, our first milestone was to implement the type system without introducing typing annotations but still have it provide value to developers by finding dead code and verified bugs. This is done through the `dynamic()`\n\ntype, which in Elixir is quite different from other gradually typed languages. Let’s break it down.\n\n## The `dynamic()`\n\ntype\n\nMany gradual type systems have the `any()`\n\ntype, which, from the point of view of the type system, often means “anything goes” and no type violations are reported. On the other hand, Elixir’s gradual type is called `dynamic()`\n\nand it has two important properties: compatibility and narrowing.\n\nIn static type systems, when you have a type of shape `integer() or binary()`\n\nand you invoke a function, said function must accept both types. However, because type systems cannot capture the intention of all of our programs with precision, this may lead to false positives. For example, take the simple code below:\n\n``` python\ndef percentage_or_error(value) when is_integer(value) do\n  value_or_error =\n    if value > 1 do\n      value\n    else\n      \"not well\"\n    end\n\n  # ... more code ...\n\n  if value > 1 do\n    value_or_error / 100\n  else\n    String.upcase(value_or_error)\n  end\nend\n```\n\nAlthough `value_or_error`\n\nhas type `integer() or binary()`\n\n, the operator `/`\n\naccepts only numbers, and `String.upcase`\n\naccepts only binaries/strings, the program above is valid and emits no exceptions at runtime. However, a type system would still report two violations, because the types supplied to `/`\n\nand `String.upcase`\n\nare not a subtype of the accepted types.\n\nWhile the program above could be better written to have no typing violations, type systems will always reject valid programs, and if Elixir were to introduce too many false positives in existing codebases, it would quickly erode the trust in the type system. Therefore, Elixir’s gradual type system tags the `value_or_error`\n\nvariable above with the type `dynamic(integer() or binary())`\n\n, which means the type is either `integer() or binary()`\n\nat runtime.\n\nWhen calling a function with a `dynamic()`\n\ntype, Elixir will only emit a typing violation if the supplied types and the accepted types are disjoint. In the program above, even though `/`\n\nexpects only numbers, `dynamic(integer() or binary())`\n\ncan be an `integer()`\n\nand given the accepted and supplied types are not disjoint, there are no typing violations. However, if we were to change the program to this:\n\n```\nvalue_or_error =\n  if value > 1 do\n    value\n  else\n    \"not well\"\n  end\n\nMap.fetch!(value_or_error, :some_key)\n```\n\nBecause `Map.fetch!`\n\nexpects a map data structure, and `value_or_error`\n\ncan only be integer or binary at runtime, the accepted and supplied types are disjoint, which turns into a violation. This is known as the compatibility property and it explains how Elixir reports only *verified bugs*.\n\nHowever, reporting only verified bugs would not be useful if we can’t find many bugs in the first place. We addressed this problem by making sure Elixir’s dynamic type can be narrowed. Take this code:\n\n``` python\ndef add_a_and_b(data) do\n  data.a + data.b\nend\n```\n\nIn the program above, `data`\n\nstarts as a `dynamic()`\n\ntype. We then use it as `data.a`\n\nand `data.b`\n\ninside the plus operator, so Elixir will refine the `data`\n\nvariable to have type `%{..., a: number(), b: number()}`\n\n, which implies it is a map with both `a`\n\nand `b`\n\nfields with number values (and potentially any other field, hence the leading `...`\n\n). Therefore, if you were to forget to select the `.b`\n\nfield and write this:\n\n``` python\ndef add_a_and_b(data) do\n  data.a + data\nend\n```\n\n`data`\n\nwould be first narrowed to a map of shape `%{..., a: number()}`\n\n, then attempted to be used as a `number()`\n\n, which would emit a violation.\n\nIn other words, the `dynamic()`\n\ntype in Elixir effectively works as a range, which can be refined as it is used throughout the program and reports violations whenever type checks fall outside of the range. This is a contrast to other gradual type systems, which use the dynamic type to discard all type information.\n\nBehind the scenes, our type inference and type checking algorithms behave as if we annotated all argument types as `dynamic()`\n\n. Once we introduce user-supplied type annotations, Elixir’s type system will behave as any statically typed language as long as `dynamic()`\n\nis not used. And whenever you cross the static-dynamic boundary, we [developed new techniques that ensure our gradual typing is sound, without a need for additional runtime checks](/blog/2023/09/20/strong-arrows-gradual-typing/).\n\n## Typing guards, clauses, and more\n\nMost of the work behind this release was to introduce type checking and narrowing to several constructs. Let’s see some of them.\n\nWhen it comes to guards, we can infer unions, intersections, and negations:\n\n``` python\ndef example(x, y) when is_list(x) and is_integer(y)\n```\n\nThe code above correctly infers `x`\n\nis a list and `y`\n\nis an integer.\n\n``` python\ndef example({:ok, x} = y) when is_binary(x) or is_integer(x)\n```\n\nThe one above infers x is a binary or an integer, and `y`\n\nis a two element tuple with `:ok`\n\nas first element and a binary or integer as second.\n\n``` python\ndef example(x) when is_map_key(x, :foo)\n```\n\nThe code above infers `x`\n\nis a map which has the `:foo`\n\nkey, represented as `%{..., foo: dynamic()}`\n\n. Remember the leading `...`\n\nindicates the map may have other keys.\n\n``` python\ndef example(x) when not is_map_key(x, :foo)\n```\n\nAnd the code above infers `x`\n\nis a map that does not have the `:foo`\n\nkey, which has the type: `%{..., foo: not_set()}`\n\n. Hence `x.foo`\n\nwithin the function body will raise a typing violation.\n\nYou can also have expressions that assert on the size of data structures:\n\n``` python\ndef example(x) when tuple_size(x) < 3\n```\n\nElixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)`\n\nwill emit a typing violation. For maps and lists, we convert size checks into emptiness ones. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code.\n\nWhen it comes to constructs such as `case`\n\nand conditionals, Elixir uses information from previous clauses to refine subsequent ones:\n\n``` php\ncase System.get_env(\"SOME_VAR\") do\n  nil -> :not_found\n  value -> {:ok, String.upcase(value)}\nend\n```\n\n`System.get_env(\"SOME_VAR\")`\n\nreturns either `nil`\n\nor a `binary()`\n\n. Because the first clause matches on `nil`\n\n, the type system knows `value`\n\ncan no longer be `nil`\n\n, and therefore it must only be a `binary()`\n\n, which allows the second clause to also type check without violations. Narrowing across clauses also helps the type system find redundant clauses and dead code in existing codebases.\n\nFurthermore, we have typed many functions in the standard library that work with tuples and maps. You can find more details in the [release notes](https://github.com/elixir-lang/elixir/releases/tag/v1.20.0).\n\n## Compilation time improvements\n\nElixir v1.20 also improves compilation times once more, especially on applications running on machines with many cores. [Even though BEAM languages are efficient to compile in general, our synthetic benchmarks now place Elixir’s build tool as the fastest among them](https://github.com/josevalim/langcompilebench). If you would like to contribute more examples and scenarios, please start a discussion so we can provide a transparent suite of benchmarks and results.\n\nIt also introduces a new compiler option called `:module_definition`\n\n, which specifies if the module definition should be `:compiled`\n\n(the default) or `:interpreted`\n\n. This may improve compilation times in large projects and it does not affect the `.beam`\n\nfiles written to disk, only how the contents inside `defmodule`\n\nare executed. You can enable it by setting `elixirc_options: [module_definition: :interpreted]`\n\nin your `mix.exs`\n\n. [Read the documentation to learn more](https://elixir.hexdocs.pm/1.20.0/Code.html#put_compiler_option/2).\n\n## What is next?\n\nThe biggest question ahead of us is: when will Elixir introduce new type signatures that leverage set-theoretic types? As recently discussed [in my ElixirConf EU 2026 keynote](https://youtu.be/Ay-gnCqDw9o?t=2389), we still have both research and development work ahead of us. We will only introduce type signatures:\n\n- if we are satisfied with the type system performance in Elixir v1.20 (and we have done\n[extensive](https://elixir-lang.org/blog/2025/12/02/lazier-bdds-for-set-theoretic-types/)[work](https://elixir-lang.org/blog/2026/02/26/eager-literal-intersections/)[optimizing](https://elixir-lang.org/blog/2026/03/19/lazy-bdds-with-eager-literal-differences/)it) - if we can implement recursive types efficiently\n- if we can implement parametric types efficiently\n- if we can implement traversing key-value pairs of maps as an enumerable efficiently (we are still researching the possible solutions here)\n\nOnce those problems are tackled, we will start to explore and discuss typed struct definitions and finally type signatures. As usual, we will keep the community posted through news and [in the Elixir Forum](https://elixirforum.com/).\n\nWe appreciate everyone who tried the release candidates, ran benchmarks, and gave us feedback! Give Elixir v1.20 a try and remember to fix all of the bugs it will find for free!", "url": "https://wpnews.pro/news/elixir-v1-20-released-now-a-gradually-typed-language", "canonical_source": "https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/", "published_at": "2026-06-03 19:05:36+00:00", "updated_at": "2026-06-03 19:50:50.182800+00:00", "lang": "en", "topics": ["ai-research"], "entities": ["Elixir", "CNRS"], "alternates": {"html": "https://wpnews.pro/news/elixir-v1-20-released-now-a-gradually-typed-language", "markdown": "https://wpnews.pro/news/elixir-v1-20-released-now-a-gradually-typed-language.md", "text": "https://wpnews.pro/news/elixir-v1-20-released-now-a-gradually-typed-language.txt", "jsonld": "https://wpnews.pro/news/elixir-v1-20-released-now-a-gradually-typed-language.jsonld"}}