TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.
Elixir
533
832 commits
updated Jan 27, 2025
Prefer to watch a presentation instead of reading? See "TypeCheck: Effortless Runtime Type Checking" - Marten Wijnja - ElixirConf EU 2022.
We add use TypeCheck to a module
and wherever we want to add runtime type-checks
we replace the normal calls to @type and @spec with @type! and @spec! respectively.
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
opaque from documentation|, a..b etc.<<>>, <<_ :: size>>, <<_ :: _ * unit>>, <<_ :: size, _ :: _ * unit>>)when to add guards to typedefs for more power.opaque and typep from documentationlazy is introduced to allow to defer type expansion to runtime (to within the check).impl(ProtocolName) to work with 'any type implementing protocol Protocolname'.
@type/@opaque/@typep-injection off for the cases in which it generates improper results.enable_runtime_checks).(-> result_type)(...-> result_type)(param_type, param2_type -> result_type)required(type) or optional(type).String.t,Enum.t, Range.t, MapSet.t etc.) (75% done) Detailsrequired(type) and optional(type) syntaxes.TypeCheck is available in Hex. The package can be installed
by adding type_check to your list of dependencies in mix.exs:
def deps do
[
{:type_check, "~> 0.13.3"},
# To allow spectesting and property-testing data generators (optional):
{:stream_data, "~> 0.5.0", only: :test},
]
end
The documentation can be found at https://hexdocs.pm/type_check.
TypeCheck exports a couple of macros that you might want to use without parentheses. To make mix format respect this setting, add import_deps: [:type_check] to your .formatter.exs file.
The full changelog can be found here
TypeCheck is by no means the other solution out there to reduce the number of bugs in your code.
Elixir's builtin type-specifications use the same syntax as TypeCheck. They are however not used by the compiler or the runtime, and therefore mainly exist to improve your documentation.
Besides documentation, extra external tools like Dialyzer can be used to perform static analysis of the types used in your application.
Dialyzer is an opt-in static analysis tool. This means that it can point out some inconsistencies or bugs, but because of its opt-in nature, there are also many problems it cannot detect, and it requires your dependencies to have written all of their typespecs correctly.
Dialyzer is also (unfortunately) infamous for its at times difficult-to-understand error messages.
An advantage that Dialyzer has over TypeCheck is that its checking is done without having to execute your program code (thus not having any effect on the runtime behaviour or efficiency of your projects).
Because TypeCheck adds @type, @typep, @opaque and @spec-attributes based on the types that are defined, it is possible to use Dialyzer together with TypeCheck.
Norm is an Elixir library for specifying the structure of data that can be used for both validation and data-generation.
On a superficial level, Norm and TypeCheck seem similar. However, there are important differences in their design considerations.
137 followers · starred Nov 2021
400 followers · starred Sep 2021
180 followers · starred Jul 2020
474 followers · starred Aug 2022
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.
Elixir
533
832 commits
updated Jan 27, 2025
Prefer to watch a presentation instead of reading? See "TypeCheck: Effortless Runtime Type Checking" - Marten Wijnja - ElixirConf EU 2022.
We add use TypeCheck to a module
and wherever we want to add runtime type-checks
we replace the normal calls to @type and @spec with @type! and @spec! respectively.
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
opaque from documentation|, a..b etc.<<>>, <<_ :: size>>, <<_ :: _ * unit>>, <<_ :: size, _ :: _ * unit>>)when to add guards to typedefs for more power.opaque and typep from documentationlazy is introduced to allow to defer type expansion to runtime (to within the check).impl(ProtocolName) to work with 'any type implementing protocol Protocolname'.
@type/@opaque/@typep-injection off for the cases in which it generates improper results.enable_runtime_checks).(-> result_type)(...-> result_type)(param_type, param2_type -> result_type)required(type) or optional(type).String.t,Enum.t, Range.t, MapSet.t etc.) (75% done) Detailsrequired(type) and optional(type) syntaxes.TypeCheck is available in Hex. The package can be installed
by adding type_check to your list of dependencies in mix.exs:
def deps do
[
{:type_check, "~> 0.13.3"},
# To allow spectesting and property-testing data generators (optional):
{:stream_data, "~> 0.5.0", only: :test},
]
end
The documentation can be found at https://hexdocs.pm/type_check.
TypeCheck exports a couple of macros that you might want to use without parentheses. To make mix format respect this setting, add import_deps: [:type_check] to your .formatter.exs file.
The full changelog can be found here
TypeCheck is by no means the other solution out there to reduce the number of bugs in your code.
Elixir's builtin type-specifications use the same syntax as TypeCheck. They are however not used by the compiler or the runtime, and therefore mainly exist to improve your documentation.
Besides documentation, extra external tools like Dialyzer can be used to perform static analysis of the types used in your application.
Dialyzer is an opt-in static analysis tool. This means that it can point out some inconsistencies or bugs, but because of its opt-in nature, there are also many problems it cannot detect, and it requires your dependencies to have written all of their typespecs correctly.
Dialyzer is also (unfortunately) infamous for its at times difficult-to-understand error messages.
An advantage that Dialyzer has over TypeCheck is that its checking is done without having to execute your program code (thus not having any effect on the runtime behaviour or efficiency of your projects).
Because TypeCheck adds @type, @typep, @opaque and @spec-attributes based on the types that are defined, it is possible to use Dialyzer together with TypeCheck.
Norm is an Elixir library for specifying the structure of data that can be used for both validation and data-generation.
On a superficial level, Norm and TypeCheck seem similar. However, there are important differences in their design considerations.
137 followers · starred Nov 2021
400 followers · starred Sep 2021
180 followers · starred Jul 2020
474 followers · starred Aug 2022