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
Hey everyone!
Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application.
This library uses Erlang esaml to provide
plug enabl...
New
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries.
offset-based pagination with...
New
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub.
Docs are at OpenaiEx User Gu...
New
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly.
One of i...
New
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
Phoenix components for pagination, sortable tables and filter forms with Flop and (optionally) Ecto.
pagination
cursor pagination
sorta...
New
Other Trending Topics
I am seeing a lot of aplications of Argumentum ad Vericundiam in software discussions. They do link some piece of writing and point us to...
New
Hey folks,
I just published a post about Hologram’s funding and where the project goes next - the short version:
Curiosum as Main Spons...
New
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog
It says that Fly is going all-in on sprites, which is a worry ...
New
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using.
We’re particularly inte...
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
I was thinking since Goatmire Elixir turned out pretty good I should maybe do another one. 30th of Sep - 2nd of Oct this year./
The firs...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #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).