Qqwy
TypeCheck - Fast and flexible runtime type-checking for your Elixir projects
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.
Core ideas
- Type- and function specifications are constructed using (essentially) the same syntax as Elixir’s built-in typespecs.
- When a value does not match a type check, the user is shown human-friendly error messages.
- Types and type-checks are generated at compiletime.
- This means type-checking code is optimized rigorously by the compiler.
- Property-checking generators can be extracted from type specifications without extra work.
- Flexibility to add custom checks: Subparts of a type can be named, and ‘type guards’ can be specified to restrict what values are allowed to match that refer to these types.
Usage Example
defmodule User do
use TypeCheck
defstruct [:name, :age]
@type! t :: %User{name: binary, age: integer}
end
defmodule AgeCheck do
use TypeCheck
@spec! user_older_than?(User.t, integer) :: boolean
def user_older_than?(user, age) do
user.age >= age
end
end
Now we can try the following:
iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 11}, 10)
true
iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 9}, 10)
false
So far so good. Now let’s see what happens when we pass values that are incorrect:
iex> AgeCheck.user_older_than?("foobar", 42)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `"foobar"`.
Details:
The call `user_older_than?("foobar", 42)`
does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()}, integer()) :: boolean()`. Reason:
parameter no. 1:
`"foobar"` does not check against `%User{age: integer(), name: binary()}`. Reason:
`"foobar"` is not a map.
(type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: nil, age: 11}, 10)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: 11, name: nil}`.
Details:
The call `user_older_than?(%User{age: 11, name: nil}, 10)`
does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()}, integer()) :: boolean()`. Reason:
parameter no. 1:
`%User{age: 11, name: nil}` does not check against `%User{age: integer(), name: binary()}`. Reason:
under key `:name`:
`nil` is not a binary.
(type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: "Aaron", age: nil}, 10)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: nil, name: "Aaron"}`.
Details:
The call `user_older_than?(%User{age: nil, name: "Aaron"}, 10)`
does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()}, integer()) :: boolean()`. Reason:
parameter no. 1:
`%User{age: nil, name: "Aaron"}` does not check against `%User{age: integer(), name: binary()}`. Reason:
under key `:age`:
`nil` is not an integer.
(type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: "José", age: 11}, 10.0)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 2 does not adhere to the spec `integer()`.
Rather, its value is: `10.0`.
Details:
The call `user_older_than?(%User{age: 11, name: "José"}, 10.0)`
does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()}, integer()) :: boolean()`. Reason:
parameter no. 2:
`10.0` is not an integer.
(type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
And if we were to introduce an error in the function definition:
defmodule AgeCheck do
use TypeCheck
@spec! user_older_than?(User.t, integer) :: boolean
def user_older_than?(user, age) do
user.age
end
end
Then we get a nice error message explaining that problem as well:
** (TypeCheck.TypeError) The call to `user_older_than?/2` failed,
because the returned result does not adhere to the spec `boolean()`.
Rather, its value is: `26`.
Details:
The result of calling `user_older_than?(%User{age: 26, name: "Marten"}, 10)`
does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()}, integer()) :: boolean()`. Reason:
Returned result:
`26` is not a boolean.
(type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
While TypeCheck is not stable yet, it is mature enough to be used for simple tasks.
Please try it out and share your experiences and feedback here! ![]()
If you like videos, also see my 2022 ElixirConf.EU talk about TypeCheck:
Most Liked
Qqwy
Version 0.11.0 has been released! ![]()
Wooh, this is a big release!
Most important features:
- We now support all of Elixir’s builtin basic types!

- We now support all of the remote types of the Elixir standard library!

- Support for most of the map-type syntactic sugars.

- An optional Credo check to enforce that all your functions have a spec.

Full changelog
Additions
- Support for fancier map syntaxes:
%{required(key_type) => value_type}Maps with a single kind of required key-value type.%{optional(key_type) => value_type}Maps with a single kind of optional key-value type.%{:some => a(), :fixed => b(), :keys => c(), optional(atom()) => any()}Maps with any number of fixed keys and a single optional key-value type.- TypeCheck now supports nearly all kinds of map types that see use. Archaic combinations of optional and required are not supported, but also not very useful types in practice.
- Because of this, the inspection of the builtin type
map(key, value)has been changed to look the same as an optional map. This is a minor backwards-incompatible change.
- Desugaring
%{}has changed from ‘any map’ to ‘the empty map’ in line with Elixir’s Typespecs. This is a minor backwards-incompatible change. - Support for the builtin types
port(),reference()and (based on these)identifier(). - Support for the builtin type
struct(). - Support for the builtin type
timeout(). - Support for the builtin type
nonempty_charlist()andmaybe_improper_listand (based on these)iolist()andiodata(). - Adding types depending on these builtins to the default type overrides. We now support all modules of the full standard library!
TypeCheck.Credo.Check.Readability.Specs: an opt-in alternative Credo check which will check whether all functions have either a@spec!or ‘normal’@spec. (Fixes #102).
Fixes
- The
TypeCheck.Builtinmodule is now actually spectested itself. Some consistency bugs were found and solved as a result.
We’re very near to a stable 1.0 release now.
But that is not all: in the meantime, some great work is being done by @orsinium to increase interoptability with modules which are not part of your own codebase.
In the near future (which will be part of the next release) we’ll be able to extract types from external modules and combine them with the ones explicitly written out using TypeCheck.
Work on this is still in progress; there are some design choices to still be made. If you find this interesting or have opinions about it, please let us know!
Qqwy
ElixirConf.EU was a lot of fun!
Being with such a nice group of cool, clever and excited people is a really great way to get new energy to continue any project.
Many people asked questions and gave valuable feedback.
Thank you very much, everyone! ![]()
(For who was not there: A video recording of the talk will be released some time later in this year, once the organisers are ready to do so.)
Qqwy
For who wants some more in-depth knowledge on how the library works and could be used, be sure to check out Episode 72 of the Thinking Elixir podcast, in which Mark Ericksen, David Bernheisel and Cade Ward interviewed me about the library
!
Qqwy
Version 0.5.0 has been released! 
This version adds a number of stability and ‘quality of life’ improvements.
Additions & Improvements
- Adding the option
debug: true, wich can be passed touse TypeCheckorTypeCheck.conform/3(and variants), which will (at compile-time) print the checks that TypeCheck is generating. Example. - Allow disabling the generation of a typespec, by writing
@autogen_typespec false. This will ensure that no typespec is exported for the next@type!/@opaque/@spec!encountered in a module. Example. - Actually by default autogenerate a
@spectypespec for all@spec!'s. - Code coverage of the test-suite increased to > 85%.
Fixes
- Bugfixes w.r.t. generating typespecs that Elixir/Dialyzer is happy with.
- Fixes compiler-warnings on unused named types when using a type guard.
- Fixes any warnings that were triggered during the test suite before.
Meta
- Moving from Travis CI to GitHub Workflows
- Setting up Coveralls for code coverage.
Qqwy
It was long overdue:
Version 0.4.0 has been released! ![]()
This adds two main features:
- Protocol-based types
- Type overrides.
Protocol-based types
This adds supports for impl(ProtocolName). This is a way to use “any type that implements protocol ProtocolName” in your types and specs.
TypeCheck supports this both in type-checks (checking whether a protocol is implemented for the given term), as well as for property-testing generation (generating “any value of any type that implements ProtocolName”):
An example of using protocols in specs:
defmodule OverrideExample do
use TypeCheck
@spec! average(impl(Enumerable)) :: {:ok, float()} | {:error, :empty}
def average(enumerable) do
if Enum.empty?(enumerable) do
{:error, :empty}
else
res = Enum.sum(enumerable) / Enum.count(enumerable)
{:ok, res}
end
end
end
OverrideExample.average([10, 20])
{:ok, 15.0}
iex(18)> OverrideExample.average(MapSet.new([1,2,3,4]))
{:ok, 2.5}
OverrideExample.average([])
{:error, :empty}
OverrideExample.average(10)
** (TypeCheck.TypeError) At iex:11:
The call to `average/1` failed,
because parameter no. 1 does not adhere to the spec `impl(Enumerable)`.
Rather, its value is: `10`.
Details:
The call `average(10)`
does not adhere to spec `average(impl(Enumerable)) :: {:ok, float()} | {:error, :empty}`. Reason:
parameter no. 1:
`10` does not implement the protocol `Elixir.Enumerable`
lib/type_check/spec.ex:156: OverrideExample.average/1
And an example of some generating some data:
iex> require TypeCheck.Type
iex> import TypeCheck.Builtin
iex> TypeCheck.Type.build(impl(Enumerable)) |> TypeCheck.Protocols.ToStreamData.to_gen |> Enum.take(5)
[
%{{false, ""} => -1.0},
#MapSet<[]>,
-2..2,
0..3,
%{:EG => {}, {false} => 1.0, [] => %{-1 => ""}}
]
Type Overrides
From time to time we need to interface with modules written in other libraries (or the Elixir standard library) which do not expose their types through TypeCheck yet.
We want to be able to use those types in our checks, but they exist in modules that we cannot change ourselves.
The solution is to allow a list of ‘type overrides’ to be given as part of the options passed to use TypeCheck, which allow you to use the original type in your types and documentation, but have it be checked (and potentially property-generated) as the given TypeCheck-type.
An example:
defmodule Original do
@type t() :: any()
end
defmodule Replacement do
use TypeCheck
@type! t() :: integer()
end
defmodule Example do
use TypeCheck, overrides: [{&Original.t/0, &Replacement.t/0}]
@spec! times_two(Original.t()) :: integer()
def times_two(input) do
input * 2
end
end
Or indeed:
defmodule TypeOverrides do
use TypeCheck
import TypeCheck.Builtin
@opaque! custom_enum() :: impl(Enumerable)
end
defmodule Example do
use TypeCheck, overrides: [{&Enum.t/0, &TypeOverrides.custom_enum/0}]
@spec! average(Enum.t()) :: {:ok, float()} | {:error, :empty}
def average(enumerable) do
# ... (see first example of post)
end
end
As this feature is still very new, there are bound to still be some bugs or edge cases in there.
Also, it would be nice to have support by default already provided for all remote types of Elixir’s standard library.
This is something which will be added in the very near future; probably in the next release.
What is next?
A detailed long-term roadmap is available in the Readme.
In the short-term, focus is on the following:
- Improve code-coverage of the testing suite
- Move the CI from Travis to GitHub’s workflows, and test against newer (and possibly some older) Elixir versions
- Add a set of ‘default overrides’ for the common remote types that are part of the Elixir standard library (such as
Enum.t(),Range.t(),DateTime.t()etc.). - Be able to limit the depth of the generated checks, to further increase performance for production environments.








