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: