# Typespecs Patterns Patterns extracted from the Elixir standard library source code. --- ## 1. Public Type with @typedoc **Source:** `lib/elixir/lib/gen_server.ex` lines 862–896 **What it does:** Every public `@type` is preceded by a `@typedoc` that explains what the type represents, often referencing which functions use it. **Why:** Types are part of the public API. Without documentation, users must guess what a type means from its definition alone. The `@typedoc` bridges intent and implementation. **Anti-pattern:** Defining `@type` without any `@typedoc`, leaving users to decipher complex union types. **Code example:** ```elixir @typedoc "Return values of `start*` functions" @type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term} @typedoc "The GenServer name" @type name :: nil | atom | {:global, term} | {:via, module, term} @typedoc """ The server reference. This is either a plain PID or a value representing a registered name. ... """ @type server :: pid | name | {atom, node} @typedoc """ Tuple describing the client of a call request. `pid` is the PID of the caller and `tag` is a unique term used to identify the call. """ @type from :: {pid, tag :: term} ``` --- ## 2. Private Types with @typep **Source:** `lib/elixir/lib/macro.ex` lines 84, 97 **What it does:** Uses `@typep` for internal recursive type definitions that are implementation details not meant for external consumers. **Why:** Keeps the public type surface small while allowing internal type reuse. Private types can reference themselves recursively (e.g., for AST node structure) without polluting the documentation. **Anti-pattern:** Making all types public "just in case." This bloats documentation and creates implicit public API guarantees. **Code example:** ```elixir @typedoc "The inputs of a macro" @type input :: input_expr | {input, input} | [input] | atom | number | binary @typep input_expr :: {input_expr | atom, metadata, atom | [input]} @typedoc "The output of a macro" @type output :: output_expr | {output, output} | [output] | atom | number | binary | captured_remote_function | pid @typep output_expr :: {output_expr | atom, metadata, atom | [output]} ``` --- ## 3. @opaque Types (Protocol t()) **Source:** `lib/elixir/lib/protocol.ex` lines 150–168 (documentation), runtime generation **What it does:** Protocols auto-generate an opaque `@type t :: term()` type that represents "any value implementing this protocol." **Why:** The type is opaque because the concrete implementations are open-ended — any module can implement the protocol. Making it opaque prevents users from pattern-matching on the internal representation (which could be anything) and signals that you should only use the protocol functions. **Anti-pattern:** Defining `@opaque` for types where consumers *need* to destructure the value. Use `@opaque` only when the internal representation is truly hidden. **Code example:** ```elixir # Generated automatically by defprotocol — documented behavior: defprotocol Size do @doc "Calculates the size (and not the length!) of a data structure" def size(data) end # Usage in specs: @spec print_size(Size.t()) :: :ok def print_size(data) do # ... end ``` --- ## 4. Union Types in @spec Return Values **Source:** `lib/elixir/lib/gen_server.ex` lines 577–583, 647–658 **What it does:** Uses union types with descriptive tagged tuples in callback specs, making all possible return shapes explicit. **Why:** OTP callbacks accept multiple return shapes (e.g., `{:reply, ...}`, `{:noreply, ...}`, `{:stop, ...}`). Spelling out every variant in the spec enables Dialyzer and makes the contract self-documenting. **Anti-pattern:** Using `term()` as a catch-all return type when specific shapes are known. This defeats the purpose of typespecs. **Code example:** ```elixir @callback init(init_arg :: term) :: {:ok, state} | {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}} | :ignore | {:stop, reason :: term} when state: term @callback handle_call(request :: term, from, state :: term) :: {:reply, reply, new_state} | {:reply, reply, new_state, timeout | :hibernate | {:continue, continue_arg :: term}} | {:noreply, new_state} | {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg :: term}} | {:stop, reason, reply, new_state} | {:stop, reason, new_state} when reply: term, new_state: term, reason: term ``` --- ## 5. `when` Constraints in Specs **Source:** `lib/elixir/lib/kernel.ex` lines 635, 1072, 1455 **What it does:** Uses the `when` clause in `@spec` to bind type variables, expressing relationships between parameters and return values. **Why:** When the same type variable appears in multiple positions, it communicates that those values are related (same type). This is critical for generic functions like `hd/1`, `max/2`. **Anti-pattern:** Using unrelated type names in different argument positions when the same variable would express the contract more precisely. **Code example:** ```elixir @spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term @spec max(first, second) :: first | second when first: term, second: term @spec tl(nonempty_maybe_improper_list(elem, last)) :: maybe_improper_list(elem, last) | last when elem: term, last: term ``` --- ## 6. Map Types with required/optional Keys **Source:** `lib/elixir/lib/supervisor.ex` lines 602–607, 644–652 **What it does:** Uses map type syntax with `required()` and `optional()` keys to define struct-like specs where some fields have defaults. **Why:** Supervisors have a child_spec map where `:id` and `:start` are mandatory but `:restart`, `:shutdown` etc. are optional with defaults. The type system reflects this precisely. **Anti-pattern:** Using a plain `map()` type or making all keys required when some have sensible defaults. **Code example:** ```elixir @type sup_flags() :: %{ strategy: strategy(), intensity: non_neg_integer(), period: pos_integer(), auto_shutdown: auto_shutdown() } @type child_spec :: %{ required(:id) => atom() | term(), required(:start) => {module(), function_name :: atom(), args :: [term()]}, optional(:restart) => restart(), optional(:shutdown) => shutdown(), optional(:type) => type(), optional(:modules) => [module()] | :dynamic, optional(:significant) => boolean() } ``` --- ## 7. Keyword List Types for Options **Source:** `lib/logger/lib/logger.ex` lines 509–531 **What it does:** Defines option types as keyword lists with specific key-value constraints, sometimes nested. **Why:** Many Elixir APIs accept keyword options. Typing them precisely documents valid options and their expected value types without forcing users to read function documentation. **Anti-pattern:** Typing options as `keyword()` (untyped keyword list), which provides zero guidance. **Code example:** ```elixir @type configure_opts :: [ level: level(), translator_inspect_opts: Inspect.Opts.t(), sync_threshold: non_neg_integer(), discard_threshold: non_neg_integer(), truncate: non_neg_integer() | :infinity, utc_log: boolean() ] @type formatter_opts :: [ colors: [ enabled: boolean(), debug: atom(), info: atom(), warning: atom(), error: atom() ], format: String.t() | {module(), atom()}, metadata: :all | [atom()], truncate: pos_integer() | :infinity, utc_log: boolean() ] ``` --- ## 8. Parameterized Types (t/1) **Source:** `lib/elixir/lib/enum.ex` lines 58–73 (Enumerable protocol) **What it does:** Defines a parameterized type `t(_element)` that allows expressing the element type of an enumerable in function specs. **Why:** Enables downstream functions to express type flow: "takes an enumerable of integers, returns an enumerable of strings." The parameter communicates element type even though the protocol dispatch erases it at runtime. **Anti-pattern:** Always using `t()` without parameters when the element type is known and useful for documentation. **Code example:** ```elixir @typedoc """ An enumerable of elements of type `element`. This type is equivalent to `t:t/0` but is especially useful for documentation. For example, imagine you define a function that expects an enumerable of integers and returns an enumerable of strings: @spec integers_to_strings(Enumerable.t(integer())) :: Enumerable.t(String.t()) def integers_to_strings(integers) do Stream.map(integers, &Integer.to_string/1) end """ @typedoc since: "1.14.0" @type t(_element) :: t() ``` --- ## 9. Named Parameters in Specs (:: annotation) **Source:** `lib/elixir/lib/gen_server.ex` line 577, `lib/elixir/lib/supervisor.ex` line 562 **What it does:** Uses `name :: type` syntax in callback/spec parameter positions to give meaningful names to parameters. **Why:** `init(init_arg :: term)` is vastly more readable than `init(term)`. The name serves as inline documentation within the spec itself. **Anti-pattern:** Writing specs with only types and no parameter names, especially for callbacks that users must implement. **Code example:** ```elixir @callback init(init_arg :: term) :: {:ok, state} | {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}} | :ignore | {:stop, reason :: term} when state: term @callback terminate(reason, state :: term) :: term when reason: :normal | :shutdown | {:shutdown, term} | term ``` --- ## 10. @typedoc since: Annotation **Source:** `lib/elixir/lib/supervisor.ex` lines 669–670, `lib/elixir/lib/enum.ex` line 72 **What it does:** Attaches a `since:` metadata annotation to `@typedoc` indicating when a type was introduced. **Why:** Helps library consumers know version requirements. The pattern mirrors `@doc since:` used on functions. **Anti-pattern:** Omitting `since:` on types added after the initial release, leaving users guessing about compatibility. **Code example:** ```elixir @typedoc since: "1.16.0" @type module_spec :: {module(), args :: term()} | module() @typedoc since: "1.14.0" @type t(_element) :: t() ```