View Source TypeCheck (TypeCheck v0.12.0)
Fast and flexible runtime type-checking.
The main way to use TypeCheck is by adding use TypeCheck
in your modules.
This will allow you to use the macros of TypeCheck.Macros
in your module,
which are versions of the normal type-specification module attributes
with an extra exclamation mark at the end: @type!
, @spec!
, @typep!
and @opaque!
.
It will also bring all functions in TypeCheck.Builtin
in scope,
which is usually what you want as this allows you to
use all types and special syntax that are built-in to Elixir
in the TypeCheck specifications.
Using these, you're able to add function-specifications to your functions which will wrap them with runtime type-checks. You'll also be able to create your own type-specifications that can be used in other type- and function-specifications in the same or other modules later on:
defmodule User do
use TypeCheck
defstruct [:name, :age]
@type! age :: non_neg_integer()
@type! t :: %User{name: binary(), age: age()}
@spec! new(binary(), age()) :: t()
def new(name, age) do
%User{name: name, age: age}
end
@spec! old_enough?(t(), age()) :: boolean()
def old_enough?(user, limit) do
user.age >= limit
end
end
Finally, you can test whether your functions correctly adhere to their specs,
by adding a spectest
in your testing suite. See TypeCheck.ExUnit.spectest/2
for details.
types-and-their-syntax
Types and their syntax
TypeCheck allows types written using (essentially) the same syntax as Elixir's builtin typespecs. This means the following:
- literal values like
:ok
,10.0
or"my string"
desugar to a call toTypeCheck.Builtin.literal/1
, which is a type that matches only exactly that value. - Basic types like
integer()
,float()
,atom()
etc. are directly supported (and exist as functions inTypeCheck.Builtin
). - tuples of types like
{atom(), integer()}
are supported (and desugar toTypeCheck.Builtin.fixed_tuple/1
) - maps where keys are literal values and the values are types like
%{a: integer(), b: integer(), 42 => float()}
desugar to calls toTypeCheck.Builtin.fixed_map/1
.- The same happens with structs like
%User{name: binary(), age: non_neg_integer()}
- The same happens with structs like
sum types like
integer() | string() | atom()
are supported, and desugar to calls toTypeCheck.Builtin.one_of/1
.- Ranges like
lower..higher
are supported, matching integers within the given range. This desugars into a call toTypeCheck.Builtin.range/1
.
currently-unsupported-features
Currently unsupported features
The following typespec syntax can not currently be used in TypeCheck. This will hopefully change in future versions of the library.
- Literal maps with
required(...)
andoptional(...)
keys. (TypeCheck does already support literal maps with a fixed set of keys, as well as maps with any number of key-value-pairs of fixed types. It is the special syntax that might mix these approaches that is not supported yet.)
extensions
Extensions
TypeCheck adds the following extensions on Elixir's builtin typespec syntax:
- fixed-size lists containing types like
[1, 2, integer()]
are supported, and desugar toTypeCheck.Builtin.fixed_list/1
. This example matches only lists of 3 elements where the first element is the literal1
, the second the literal2
and the last element any integer. Elixir's builtin typespecs do not support fixed-size lists. - named types like
x :: integer()
are supported; these are useful in combination with "type guards" (see the section below). - "type guards" using the syntax
some_type when arbitrary_code
are supported, to add extra arbitrary checks to a value for it to match the type. (See the section about type guards below.) lazy(some_type)
, which defers type-expansion until during runtime. This is required to be able to expand recursive types. C.f.TypeCheck.Builtin.lazy/1
named-types-type-guards
Named Types Type Guards
To add extra custom checks to a type, you can use a so-called 'type guard'. This is arbitrary code that is executed during a type-check once the type itself already matches.
You can use "named types" to refer to (parts of) the value that matched the type, and refer to these from a type-guard:
type sorted_pair :: {lower :: number(), higher :: number()} when lower <= higher
iex> TypeCheck.conforms!({10, 20}, sorted_pair)
{10, 20}
iex> TypeCheck.conforms!({20, 10}, sorted_pair)
** (TypeCheck.TypeError) `{20, 10}` does not match the definition of the named type `TypeCheckTest.TypeGuardExample.sorted_pair`
which is: `TypeCheckTest.TypeGuardExample.sorted_pair
::
(sorted_pair :: {lower :: number(), higher :: number()} when lower <= higher)`. Reason:
`{20, 10}` does not check against `(sorted_pair :: {lower :: number(), higher :: number()} when lower <= higher)`. Reason:
type guard:
`lower <= higher` evaluated to false or nil.
bound values: %{higher: 10, lower: 20, sorted_pair: {20, 10}}
Named types are available in your guard even from the (both local and remote) types that you are using in your time, as long as those types are not defined as opaque types.
manual-type-checking
Manual type-checking
If you want to check values against a type outside of the checks the @spec!
macro
wraps a function with,
you can use the conforms/2
/conforms?/2
/conforms!/2
macros in this module directly in your code.
These are evaluated at compile time which means the resulting checks will be optimized by the compiler. Unfortunately it also means that the types passed to them have to be known at compile time.
If you have a type that is constructed dynamically at runtime, you can resort to
dynamic_conforms/2
and variants.
Because these variants have to evaluate the type-checking code at runtime,
these checks are not optimized by the compiler.
introspection
Introspection
To allow checking what types and specs exist,
the introspection function __type_check__/1
will be added to a module when use TypeCheck
is used.
YourModule.__type_check__(:types)
returns a keyword list of all{type_name, arity}
pairs for all types defined in the module using@type!
/@typep!
or@opaque!
.YourModule.__type_check__(:specs)
returns a keyword list of all{function_name, arity}
pairs for all functions in the module wrapped with a spec using@spec!
.
Note that these lists might also contain private types / private function names.
Link to this section Summary
Functions
Similar to conforms/2
, but returns value
if the value typechecked and raises TypeCheck.TypeError if it did not.
Makes sure value
typechecks the type description type
.
Similar to conforms/2
, but returns true
if the value typechecked and false
if it did not.
Similar to dynamic_conforms/2
, but returns value
if the value typechecked and raises TypeCheck.TypeError if it did not.
Makes sure value
typechecks the type type
. Evaluated at runtime.
Similar to dynamic_conforms/2
, but returns true
if the value typechecked and false
if it did not.
Link to this section Types
@type value() :: any()
Link to this section Functions
conforms!(value, type, options \\ Macro.escape(TypeCheck.Options.new()))
View Source (macro)Similar to conforms/2
, but returns value
if the value typechecked and raises TypeCheck.TypeError if it did not.
The same features and restrictions apply to this function as to conforms/2
.
conforms(value, type, options \\ Macro.escape(TypeCheck.Options.new()))
View Source (macro)Makes sure value
typechecks the type description type
.
If it typechecks, we return {:ok, value}
Otherwise, we return {:error, %TypeCheck.TypeError{}}
which contains information
about why the value failed the check.
conforms
is a macro and expands the type check at compile-time,
allowing it to be optimized by the compiler.
C.f. TypeCheck.Type.build/1
for more information on what type-expressions
are allowed as type
parameter.
Note: usually you'll want to import TypeCheck.Builtin
in the context where you use conforms
,
which will bring Elixir's builtin types into scope.
(Calling use TypeCheck
will already do this; see the module documentation of TypeCheck
for more information))
conforms?(value, type, options \\ Macro.escape(TypeCheck.Options.new()))
View Source (macro)Similar to conforms/2
, but returns true
if the value typechecked and false
if it did not.
The same features and restrictions apply to this function as to conforms/2
.
dynamic_conforms!(value, type, options \\ TypeCheck.Options.new())
View SourceSimilar to dynamic_conforms/2
, but returns value
if the value typechecked and raises TypeCheck.TypeError if it did not.
The same features and restrictions apply to this function as to dynamic_conforms/2
.
iex> fourty_two = TypeCheck.Type.build(42)
iex> TypeCheck.dynamic_conforms!(42, fourty_two)
42
iex> TypeCheck.dynamic_conforms!(20, fourty_two)
** (TypeCheck.TypeError) At lib/type_check.ex:323:
`20` is not the same value as `42`.
Makes sure value
typechecks the type type
. Evaluated at runtime.
Because dynamic_conforms/2
is evaluated at runtime:
The typecheck cannot be optimized by the compiler, which makes it slower.
You must pass an already-expanded type as
type
. This can be done by using one of your custom types directly (e.g.YourModule.typename()
), or by callingTypeCheck.Type.build
.
Use dynamic_conforms
only when you cannot use the normal conforms/2
,
for instance when you're only able to construct the type to check against at runtime.
iex> fourty_two = TypeCheck.Type.build(42)
iex> TypeCheck.dynamic_conforms(42, fourty_two)
{:ok, 42}
iex> {:error, type_error} = TypeCheck.dynamic_conforms(20, fourty_two)
iex> type_error.message
"At (for doctest at) lib/type_check.ex:260:
`20` is not the same value as `42`."
dynamic_conforms?(value, type, options \\ TypeCheck.Options.new())
View SourceSimilar to dynamic_conforms/2
, but returns true
if the value typechecked and false
if it did not.
The same features and restrictions apply to this function as to dynamic_conforms/2
.
iex> fourty_two = TypeCheck.Type.build(42)
iex> TypeCheck.dynamic_conforms?(42, fourty_two)
true
iex> TypeCheck.dynamic_conforms?(20, fourty_two)
false