Qqwy

Qqwy

TypeCheck Core Team

TypeCheck - Fast and flexible runtime type-checking for your Elixir projects

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

hex.pm version Documentation ci Coverage Status

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! :slight_smile:


If you like videos, also see my 2022 ElixirConf.EU talk about TypeCheck:

336 13802 100

Most Liked

Qqwy

Qqwy

TypeCheck Core Team

Version 0.11.0 has been released! :rocket:

Wooh, this is a big release!

Most important features:

  • We now support all of Elixir’s builtin basic types! :confetti_ball:
  • We now support all of the remote types of the Elixir standard library! :partying_face:
  • Support for most of the map-type syntactic sugars. :sunglasses:
  • An optional Credo check to enforce that all your functions have a spec. :heavy_check_mark:

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() and maybe_improper_list and (based on these) iolist() and iodata().
  • 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.Builtin module 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

Qqwy

TypeCheck Core Team

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! :partying_face:

(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

Qqwy

TypeCheck Core Team

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 :blush: !

Qqwy

Qqwy

TypeCheck Core Team

Version 0.5.0 has been released! :blush:

This version adds a number of stability and ‘quality of life’ improvements.

Additions & Improvements

  • Adding the option debug: true, wich can be passed to use TypeCheck or TypeCheck.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 @spec typespec 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

Qqwy

TypeCheck Core Team

It was long overdue:
Version 0.4.0 has been released! :cake:

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.

Where Next?

Popular in Libraries Top

mhanberg
I just released the first version of Temple: an HTML DSL for Elixir and Phoenix! You can read this blog post or the docs for more info...
New
Qqwy
While not as prevalent as in imperative languages, arrays (collections with efficient random element access) are still very useful in Eli...
New
mcrumm
If you would like to migrate away from node/npm/webpack while still using sass, the dart_sass package provides a installer and runner for...
New
praveenperera
FastRSS Parse RSS feeds very quickly: This is rust NIF built using rustler Uses the RSS rust crate to do the actual RSS parsing Speed...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
deadtrickster
I’ve just released stable versions of my Prometheus Elixir libs: Elixir client [docs]; Ecto collector [docs]; Plugs instrumenter/Export...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: https://github.com/devonestes/assertions ...
New
oltarasenko
Dear Elixir community, After a year of development, bug fixes, and improvements, we are proudly ready to share the release of Crawly 0.1...
New
kelvinst
There was a feature proposal recently on Elixir core mailing list where an idea born. I got very interested in the idea, but it is not go...
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Sub Categories:

We're in Beta

About us Mission Statement