View Source Comparing TypeCheck and Elixir Typespecs

TypeCheck is intended to be a drop-in supplement to Elixir typespecs. With very few exceptions, all of Elixir's typespecs are supported by TypeCheck. Below is a breakdown of supported typespec syntax in TypeCheck.

In the tables below:

  • '✅' indicates that something is supported
  • '❌' indicates that something is not supported.
  • '⌛' indicates that something is not currently supported, but there are plans to add support for it in the near future.

basic-types

Basic Types

TypeSupported?Notes
any()the top type, the set of all terms
none()the bottom type, contains no terms
atom()
map()any map
pid()process identifier
port()port identifier
reference()
tuple()tuple of any size
float()
integer()
neg_integer()..., -3, -2, -1
non_neg_integer()0, 1, 2, 3, ...
pos_integer()1, 2, 3, ...
list(type)proper list
nonempty_list(type)non-empty proper list
maybe_improper_list(content_type, termination_type)proper or improper list
nonempty_improper_list(content_type, termination_type)improper list
nonempty_maybe_improper_list(content_type, termination_type)non-empty proper or improper list

literals

Literals

TypeSupported?Notes
true
false
nil
<<>>empty bitstring
<<_::size>size is 0 or a positive integer
<<_::_*unit>>unit is an integer from 1 to 256
<<_::size, _::_*unit>>
(-> type)✅¹0-arity, returns type
(type1, type2 -> type)✅¹2-arity, returns type
(... -> type)✅¹any arity, returns type
1integer
1..10range
[type]list with any number of type elements
[]empty list
[...]shorthand for nonempty_list(any())
[type, ...]shorthand for nonempty_list(type)
[key: value_type]keyword list with key :key of value_type
%{}empty map
%{key: value_type}map with required (atom) key :key of value_type
%{key_type => value_type}map with required pairs of key_type and value_type
%{required(key_type) => value_type}✅²map with required pairs of key_type and value_type
%{optional(key_type) => value_type}✅²map with optional pairs of key_type and value_type
%SomeStruct{}struct with all fields of any type
%SomeStruct{key: value_type}struct with required key :key of value_type
%{key: value_type, optional(opt_key_type) => opt_value_type}✅³struct with required key :key of value_type, and zero or more pairs of opt_key_type and opt_value_type
{}empty tuple
{:ok, type}two-element tuple with an atom and any type

¹: Functions passed as parameters can only be fully checked once they are called. TypeCheck wraps them in a 'wrapper function' which performs the correct check on their input/output. This wrapper will only run once the the function actually is called.

²: Only a single 'required' or 'optional' field in a map is currently supported. (Types which need more are fortunately very rare.)

³: Only optional is currently supported, and only a single one. (Types which need more are fortunately very rare.)

built-in-types

Built-in types

TypeSupported?Notes
term()any()
arity()0..255
as_boolean(t)t
binary()<<_::_*8>>
bitstring()<<_::_*1>>
boolean()true | false
byte()0..255
char()0..0x10FFFF
charlist()[char()]
nonempty_charlist()[char(), ...]
fun()(... -> any)
function()fun()
identifier()pid() | port() | reference()
iodata()iolist() | binary()
iolist()maybe_improper_list(byte() | binary() | iolist(), binary() | [])
keyword()[{atom(), any()}]
keyword(t)[{atom(), t}]
list()[any()]
nonempty_list()nonempty_list(any())
maybe_improper_list()maybe_improper_list(any(), any())
nonempty_maybe_improper_list()nonempty_maybe_improper_list(any(), any())
mfa(){module(), atom(), arity()}
module()atom()
no_return()none()
node()atom()
number()integer() | float()
struct()%{:__struct__ => atom(), optional(atom()) => any()}
timeout():infinity | non_neg_integer()

typecheck-additions

🚀 TypeCheck Additions

TypeCheck adds the following extensions on Elixir's builtin typespec syntax:

TypeNotes
impl(protocol_name)Checks whether the given value implements the particular protocol
fixed_list(element_types)Fixed size where element_types dictate types
tuple(size)Any types, but which has exactly size elements
map(key_type, value_type)Zero or more keys of key_type and values of value_type
type when guardA 'type guard', an extra check implemented in arbitrary code (see below)

defining-specifications

Defining Specifications

TypeSupported?Notes
@typeas @type!
@opaqueas @opaque!
@typepas @typep!
@specas @spec!

Basic Spec Definition

# typespecs
@spec function_name(type1, type2) :: return_type

# TypeCheck
@spec! function_name(type1, type2) :: return_type

Spec Guards

# typespecs
@spec function(arg) :: [arg] when arg: atom
@spec function(arg1, arg2) :: {arg1, arg2} when arg1: atom, arg2: integer
@spec function(arg) :: [arg] when arg: var

# TypeCheck - unsupported

TypeCheck currently solely allows the usage of when to specify 'type guards' (see below). Support for spec guards could be added. However:

  • Their usage is quite rare.
  • Each place where it is used, it matches 'exactly the same value'. Building this check is relatively tricky.

If you have a strong desire for this feature, please open an issue for it.

Named Arguments

# typespecs
@spec days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer
@type color :: {red :: integer, green :: integer, blue :: integer}

# TypeCheck
@spec! days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer
@type! color :: {red :: integer, green :: integer, blue :: integer}

Specification Overloads

# typespecs
@spec function(integer) :: atom
@spec function(atom) :: integer

# TypeCheck - unsupported

There is no intention to support this. The implementation would be very difficult, and it is arguably good practice to have a single specification anyway.

user-defined-types

User Defined Types

Basic Definition

# typespecs
@type type_name :: type
@typep type_name :: type
@opaque type_name :: type

# TypeCheck
@type! type_name :: type
@typep! type_name :: type
@opaque! type_name :: type

Parameterized Types

# typespecs
@type dict(key, value) :: [{key, value}]

# TypeCheck
@type! dict(key, value) :: [{key, value}]

🚀 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:

# typespecs - unsupported

# TypeCheck
@type! sorted_pair :: {lower :: number(), higher :: number()} when lower <= higher

remote-types

✅ Remote Types

It is often useful to refer to types defined in other modules. These are called 'Remote types'. Elixir's typespecs and TypeCheck both support remote types.

# typespecs
defmodule User do
  @type t() :: %User{name: String.t(), age: non_negative_integer()}
end

defmodule Greeter do
  @spec greet(User.t()) :: String.t()
  def greet(user) do
   # ... 
  end
end

# TypeCheck
defmodule User do
  use TypeCheck

  @type! t() :: %User{name: String.t(), age: non_negative_integer()}
end

defmodule Greeter do
  use TypeCheck

  @spec! greet(User.t()) :: String.t()
  def greet(user) do
   # ... 
  end
end

remote-type-overrides

Remote 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.

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

elixir-standard-library-types

Elixir Standard Library Types

TypeCheck helpfully ships with the majority of the types in Elixir's Standard Library already implemented as default overrides. This means that your @spec! definitions can reference types like String.t(), Date.t() and Range.t() out of the box.

TypeSupported?Notes
Access
Agent
Application
Calendar
Calendar.ISO
Calendar.TimeZoneDatabase
Code
Collectable
Config.Provider
Date
Date.Range
DateTime
Dictdeprecated
DynamicSupervisor
Enum
Enumerable
Exception
File
File.Stat
File.Stream
Float
Function
GenEventdeprecated
GenServer
HashDictdeprecated
HashSetdeprecated
IO
IO.ANSI
IO.Stream
Inspect
Inspect.Algebra
Inspect.Opts
Keyword
List.Chars
Macro
Macro.Env
Map
MapSet
NaiveDateTime
Node
OptionParser
Path
Port
Process
Range
Regex
Registry
Setdeprecated
Stream
String
String.Chars
Supervisor
Supervisor.Specdeprecated
System
Task
Task.Supervisor
Time
URI
Version
Version.Requirement