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

Where Next?

Popular in Announcing Top

mathieuprog
Hello :waving_hand: Allow me to introduce you to Tz, an alternative time zone database support to Tzdata. Why another library? First a...
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
Eiji
ExApi is a library that I’m developing now and hope release soon This library will allow to: list all apis list all api implementation...
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12560 134
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
Jskalc
Hi! Today, after a couple weeks of development I’ve released v0.1 of LiveVue. It’s a seamless integration of Vue and Phoenix LiveView, i...
New
michalmuskala
Hello everybody. I have just released Jason - a new JSON library. You might be wondering, why do we need a new library? The primary foc...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New
scohen
Lexical Lexical is a next-generation language server for the Elixir programming language. Features Context aware code completion As-you...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement