
Fl4m3Ph03n1x
How to define Macro for a new Type?
Background
So, I am playing around with a concept named “NewType” and I am taking inspiration from languages like F# and Scala.
My objective, for learning purposes mostly, is to build a macro that makes creating this abstraction something that takes no more than a single line of code.
Intended usage
I would like to create a macro that allows me to do something like this:
defmodule User do
require NewType # an absolutely original name for the macro :D
deftype Name, String.t() # Usage of said macro. Here I am defining a new type called "Name"
@enforce_keys [:name, :age]
defstruct [:name, :age]
@type t :: %__MODULE__{
name: Name.t,
age: integer()
}
@spec new(Name.t, integer) :: User.t
def new(name, age), do: %User{name: name, age, age}
end
And now, here is how I could create a User
:
defmodule Test do
alias User
import User.Name
@spec run :: User.t
def run do
name = Name("John")
User.new(name, 25)
end
end
How to implement this interface?
This interface might remind you a little of the Record
interface. That’s because I think its API has some good ideas I would like to explore.
So, as a starting point I tried reading the source code for Record, but I was not really able to pick it up and use it to create an implementation for my use case, mainly because I don’t need/want to interface with Erlang records at all.
So, an implementation possibility would be to, under the hood, turn this into a tuple:
defmodule NewType do
defmacro new(name, val) do
quote do
NewType.to_tuple(unquote(name), unquote(val))
end
end
def to_tuple(name, val), do: {String.to_atom(name), val}
end
However, this is miles away from the interface I want to create …
Questions
- Using Elixir macros, is it possible to create the API I am aiming for?
- How can I change my code to achieve something like
Name("John")
?
Marked As Solved

Fl4m3Ph03n1x
My Answer
After reading more about macros in Elixir, talking to the community and reading about NewType
, I have refined my ideas. While the exact implementation of my original idea is not possible, with some changes you can still get the core benefit of NewType
.
Changes to original idea
- No usage of
Name("John")
syntax. As explained in this post this syntax is not valid in Elixir. - No
defguard
. Because the type is@opaque
it is not possible to have a guard that analyses the internal structure of the data without having dialyzer complaining. Since the main goal here is to have Dialyzer help me detect issues, and since the internal structure of the opaque data can only be analyzed by functions that belong to the module itself, this means this idea is not possible. - No verification on data type when invoking
new
. Originally I thought about having some verification mechanism, but this is not necessary, since dialyzer will let the user know if the user is invokingnew
with an incorrect parameter. - No self-generated functions. Instead of having
Age.age?
orName.name?
I have opted for the more generalNewType.is_type?/2
, which will accomplish the same and is more general.
Code
With these changes in mind, this is the macro I came up with:
defmodule NewType do
defmacro deftype(name, type) do
quote do
defmodule unquote(name) do
@opaque t :: {unquote(name), unquote(type)}
@spec new(value :: unquote(type)) :: t
def new(value), do: {unquote(name), value}
@spec extract(new_type :: t) :: unquote(type)
def extract({unquote(name), value}), do: value
end
end
end
@spec is_type?(data :: {atom, any}, new_type :: atom) :: boolean
def is_type?({type, _data}, new_type) when type == new_type, do: true
def is_type?(_data, _new_type), do: false
end
Which can be used like:
type.ex:
defmodule Type do
import NewType
deftype Name, String.t()
end
test.ex:
defmodule Test do
alias Type.Name
@spec print(Name.t()) :: binary
def print(name), do: Name.extract(name)
def run do
arg = 1
name = Name.new(arg) # dialyzer detects error !
{:ok, name}
end
end
Also Liked

dimitarvp
Sorry to comment on the previous idea – but that’s way too global. In a moderately big project you can have 5 different kinds of names. So to implement this properly you’d likely have to scope this somehow e.g. with module names (which are also atoms as you know):
defmodule MyApp.Types.Address.Name do
def name?({__MODULE__, val}) when is_binary(val), do: true
In many languages, internally types get all sorts of fancy prefixes. That’s important. You don’t want only one Name
to be ever allowed in your project.
Elixir offers an even neater way of doing this:
def is_type?({type, _data}, type), do: true # note the repeated `type` variable
def is_type?(_data, _new_type), do: false
Examples:
is_type?({:x, "something"}, :x} # returns true
is_type?({:x, "something"}, :y} # returns false

OvermindDL1
Elixir doesn’t really have type dispatch though as it has no static type system, so I’m unsure what any kind of NewType could accomplish with this short of just {:mytype, values}
or so as the purpose of NewType’s are to specify a unique set of functionality while hiding the original functionality of a specific type.

Fl4m3Ph03n1x
This is what I would ideally like to go for, but using a macro. The macro would convert this “type” to a tuple, and then Dialyzer would be able to pick up miss matches.
I understand the pattern of NewType as is commonly defined is mostly applicable to static languages, but that does not mean we can not learn good lessons from it and adapt to Elixir, in a way that accomplishes the same
Popular Backend topics










Other popular topics










Latest in Backend
Latest (all)
Categories:
Popular Portals
- /elixir
- /rust
- /wasm
- /ruby
- /erlang
- /phoenix
- /keyboards
- /js
- /rails
- /python
- /security
- /go
- /swift
- /vim
- /clojure
- /java
- /haskell
- /emacs
- /svelte
- /onivim
- /typescript
- /crystal
- /c-plus-plus
- /tailwind
- /kotlin
- /gleam
- /react
- /flutter
- /elm
- /ocaml
- /vscode
- /opensuse
- /ash
- /centos
- /php
- /deepseek
- /scala
- /zig
- /html
- /debian
- /nixos
- /lisp
- /agda
- /sublime-text
- /textmate
- /react-native
- /kubuntu
- /arch-linux
- /ubuntu
- /revery
- /manjaro
- /spring
- /django
- /diversity
- /nodejs
- /lua
- /julia
- /c
- /slackware
- /neovim