Qqwy
TypeCheck Core Team
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:
Trending in Announcing
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries.
offset-based pagination with...
New
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
I released Doggo, a collection of unstyled Phoenix components.
https://github.com/woylie/doggo
Features
Unstyled Phoenix components....
New
Hi there! We created Gust: A task orchestrator inspired by Airflow.
For those who have never heard about Aiflow, it’s a Python-based wor...
New
Hello
Published a new library - ProcessHub!
ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Other Trending Topics
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself.
My main conc...
New
It’s not that it’s vocabulary is too advanced. It’s something worse.
I get lost trying to follow even a paragraph written by Claude. It’...
New
This showed up on my feed.. anyone heard of it? Just hype?
Ox Alpha is a reasoning model designed for coding, sustained ag...
New
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
hauleth
What is runtime penalty for using this?
ityonemo
I recommend checking the @spec values themselves. If you’re curious how to do this I made this library (which I later decided I hated) which can inspect the type specs of remote modules: GitHub - ityonemo/typed_headers: typed headers for elixir · GitHub
Also this is superficial but in your example note the convention with regard to is_ functions and ? functions.
Qqwy
Great question! Because the type-checking code is generated and added at compile-time it gets optimized by the compiler, which means that redundant checks are eliminated from the code and you end up with something that is quite performant.
How fast it will be exactly of course depends on what kind of type you are checking against. TypeCheck performs exhaustive checking of e.g. the elements in a list, which takes longer when the list is longer.
For simple datatypes, the type-check is a constant-time operation.
As an example, the following:
is turned into something like:
(This is somewhat simplified; in the actual implementation we pass nested errors back up as values before they are raised as exception to be able to have human-friendly error messages.)
So a general answer would be ‘for most situations the performance penalty is probably negligible’.
I definitely want to write a couple of benchmarks to be able to give a more scientific answer in the future. And it might also be able to further optimize the code that is generated (allowing the compiler to do even more optimizations) in the future as well.
Qqwy
@ityonemo thank you! The example in the first post (and the README) has been updated. That’s what you get when writing documentation while burning the midnight oil
.
The main reason I chose to not read the
@spec-values themselves is to allow people to ‘opt-out’ of the@spec/@typeetc. that are being generated, which can be useful in some cases. It also allows support for certain kinds of extensions that Elixir’s builtin typespecs cannot handle, like adding named types, ‘type guards’ or certain type-shorthands liketuple(3).I do wonder what happened which made you dislike your library later on; it seems like quite a bit of effort went into writing that!
For the curious, here is an example of what kind of BEAM code ends up being generated for an example like this.
Something cool that you can see here is for instance that the return-type check is completely elided since the compiler sees that
addwill always return a number.While I’m already quite happy with this, there are a couple of potential improvements for the future I’d like to make as well:
{:ok, []}everywhere, but we can at compile-time figure out whether/where we can just use:okwhich would make the job for the compiler a lot simpler; passing a static atom around vs a 2-tuple probably makes quite a difference.:jumpcalls that are added to the bytecode, since I want the ‘happy path’ to be as fast as possible.baldwindavid
@Qqwy I’m currently using Norm, but this might end up being a better fit for my needs. I’m solely using Norm for the
@contractspecifications as a replacement for typespecs, but need to create my own functions to represent everything in typespecs, which feels a bit like recreating the wheel.Are the specs that can be created in any way limited? One of the things I like about Norm is that anything can be used. Is TypeCheck simply optimizing the parts it can and letting the rest run as it normally would?
I also had the question whether it would be possible to check @spec/@type. It appears that it is and @ityonemo has done it, but that is not a goal here.
I can’t help but envision a package that could check regular @spec/@type at runtime. Those standard typespecs could be used most of the time, but when you need to do something special you can remove the
@and write more complex specifications. Ifspeccould use remote@types that would also be cool, but no idea if that would be possible.Qqwy
Correct. TypeCheck optimizes the parts of the type it understands, and if you want to add extra checks you can add a ‘type guard’ which allows arbitrary Elixir code:
This indeed optimizes to code that does, in order, the following:
Does that answer your question?
ityonemo
I disliked it for the following reasons.
def/2) unless you really really really have a good reason to. On the other hand, if I were to rename it (defc/2) or something it could cause the reader to have to think a bit. In the end, since I’m hiring out my team, my preference is to stick as much as possible to the existing elixir standard to prevent confusion ahead of having to grow my team (especially as the hires will probably not know elixir to start off).baldwindavid
This is a really impressive and useful package. I tested it out on quite a bit of code (about 400 functions) and didn’t really run into any issues with the code. Ergonomics are the only issue for me in that
specis not syntax highlighted as nicely as something like@spec/@contractand empty newlines appear between the spec and multiline functions, which is almost always for me.I can’t help but wonder if something like
@contractor@checkrather thanspecmight solve these problems.I still think the ultimate would be if this could additionally do type checking on
@specand@type. Teams could install it and immediately get the benefit of runtime checks that are optimized. And if someone needs to do more, just remove the@andspecunleashes the power of what this package already does.Regardless, really nice work!
Qqwy
Thank you, great to hear!
I am currently trying out some tests to extract the specs from the
@spec/@type/@typep/@opaqueattributes directly. It seems promising, but I’ll need to do some more tests before I’m sure that it (i.e. overriding@) does not have some weird edge cases.baldwindavid
I went ahead and tried out your test branch applying it across my entire codebase switching all uses of
spec/typeto@spec/@type. My test suite continues to pass and I don’t seem to have hit any edge cases. I’m not sure how it could get much better than this just using@spec/@typeeverywhere. Would be neat if Elixir just worked like this out of the box (maybe with ability to disable in prod for zero cost).