# Dynamic union type as a constraint to class method argument type

**URL:** <https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121>\
**Category:** Help & Support\
**Created:** [November 18, 2022, 3:01am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121 "2022-11-18T03:01:21Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![Pan](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/pan/32/1475_2.png) [@Pan](https://forum.crystal-lang.org/u/Pan)\
**Post date:** [November 18, 2022, 3:01am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/1 "2022-11-18T03:01:21Z")

</div>

I want to define a dynamic union type `HobbyType` as a constraint to the class method `printype` argument.

```crystal
annotation Hobby
end

struct Pizza
end

struct Sport
end

struct Movie
end

class Person
  alias HobbyType = String | {{ hobby_types }}

  @[Hobby]
  @favorite : Movie

  @[Hobby]
  @eating : Pizza

  @name : String

  macro hobby_types
    {{ "Union(#{@type.instance_vars.select(&.annotation(Hobby))
        .map(&.type.id).splat})".id }}
  end

  def self.printype(hobby : HobbyType)
    pp! "#{hobby}"
  end
end

```

But the code doesn’t compile and produce this error.

```auto
 14 | alias HobbyType = String | {{ hobby_types }}
                                 ^
Error: unexpected token: "{{"

```

How to fix this and make it work?

---

<div class="post-metadata">

**Author:** ![Blacksmoke16](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/blacksmoke16/32/1241_2.png) [@Blacksmoke16](https://forum.crystal-lang.org/u/Blacksmoke16)\
**Post date:** [November 18, 2022, 4:34am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/2 "2022-11-18T04:34:29Z")

</div>

What you’re trying to do here isn’t directly possible since instance vars are only known within the context of a method, which you can’t define an alias within. You could use some sort of macro to declare the ivars and push their types to some array you use within a `macro finished` hook to build out the alias if you really wanted tho. Otherwise not sure there’s much you can do.

---

<div class="post-metadata">

**Author:** ![Pan](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/pan/32/1475_2.png) [@Pan](https://forum.crystal-lang.org/u/Pan)\
**Post date:** [November 18, 2022, 6:53am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/3 "2022-11-18T06:53:21Z")

</div>

As long as it can achieve the goal described on the subject of this topic, alternative solution to alias is OK. I’m trying to move this forward but got another error.

```crystal
annotation Hobby
end

struct Pizza
end

struct Sport
end

struct Movie
end

class Person
  #alias HobbyType = String | {{ hobby_types }}

  macro add_hobby(hb)
    @[Hobby]
    @{{ hb.var }} : {{ hb.type }}?
  end

  @[Hobby]
  @favorite : Movie?

  @[Hobby]
  @eating : Pizza?

  @name : String?

  macro union_hobby_types
    {{ "Union(#{@type.instance_vars.select(&.annotation(Hobby))
        .map(&.type.id).splat})".id }}
  end

  def self.printype(hobby : HobbyType)
    pp! "#{hobby}"
  end

  def hobby_types
    {{ union_hobby_types }}
  end
end

pp Person.new.hobby_types

```

and the error:

```auto
 39 | {{ union_hobby_types }}
         ^----------------
Error: undefined macro variable 'union_hobby_types'

```

The macro should be visible within the class. But why undefined?

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [November 18, 2022, 11:19am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/4 "2022-11-18T11:19:00Z")

</div>

Macro defs are called like regular methods. They are not visible in the macro language (inside the curly braces). Just drop the curly braces and it works.

See [Macro methods - Crystal](https://crystal-lang.org/reference/1.6/syntax_and_semantics/macros/macro_methods.html)

---

<div class="post-metadata">

**Author:** ![Pan](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/pan/32/1475_2.png) [@Pan](https://forum.crystal-lang.org/u/Pan)\
**Post date:** [November 20, 2022, 6:06am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/5 "2022-11-20T06:06:11Z")

</div>

```crystal
annotation Hobby
end

struct Pizza
end

struct Sport
end

struct Movie
end

class Person
  #alias HobbyType = String | new.hobby_types

  macro hobby(hb)
    @[Hobby]
    @{{ hb.var }} : {{ hb.type }}?
  end

  hobby favorite : Movie
  hobby eating : Pizza

  @name : String?

  macro union_hobby_types
    {{ "Union(#{@type.instance_vars.select(&.annotation(Hobby))
         .map(&.type.id).splat})".id }}
  end

  macro finished
    pp! new.hobby_types # => (Movie | Pizza | Nil)
    def self.printype(hobby : new.hobby_types)
      hobby.class
    end
  end

  def hobby_types
    union_hobby_types
  end
end
pp Person.new.hobby_types
pp Person.printype(Movie.new)

```

It seems it is almost there but another error,

```auto
 > 1 | pp! new.hobby_types # => (Movie | Pizza | Nil)
 > 2 | def self.printype(hobby : new.hobby_types)
                                 ^
Error: unexpected token: "new"

```

I expect `printype` arg `hobby` is declared as the result of `hobby_types()`. How to fix this?

---

<div class="post-metadata">

**Author:** ![asterite](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/asterite/32/60_2.png) [@asterite](https://forum.crystal-lang.org/u/asterite)\
**Post date:** [November 20, 2022, 9:43am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/6 "2022-11-20T09:43:43Z")

</div>

Hi! Could you give.an overview of what you are trying to do or solve (and not how to implement the solution you have in mind)?

---

<div class="post-metadata">

**Author:** ![Pan](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/pan/32/1475_2.png) [@Pan](https://forum.crystal-lang.org/u/Pan)\
**Post date:** [November 20, 2022, 11:39am UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/7 "2022-11-20T11:39:05Z")

</div>

I’m trying to solve a problem in [Granite](https://github.com/amberframework/granite).  
If we have a model defined as described in the [doc](http://amberframework.github.io/granite/models.html),

```auto
enum OrderStatus
  Active
  Expired
  Completed
end

class Order < Granite::Base
  connection mysql
  table foos

  # Other fields
  column status : OrderStatus, converter: Granite::Converters::Enum(OrderStatus, String) 
end

```

It will fail when running the code like `Order.create(price: 2, product_id: 9, status: OrderStatus::Active)` or `order.update(status: OrderStatus::Expired)`, because the implementation of `create` or `update` restricts the argument type(`ModelArgs`) to a list of basic types commonly known to a db. It doesn’t consider the user customized type OrderStatus.

So my code here is just a simplified version showing the issue and my attempt to fix it.

---

<div class="post-metadata">

**Author:** ![asterite](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/asterite/32/60_2.png) [@asterite](https://forum.crystal-lang.org/u/asterite)\
**Post date:** [November 20, 2022, 3:30pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/8 "2022-11-20T15:30:43Z")

</div>

Something like tthis?

```crystal
module Model
  macro included
    HOBBIES = [] of Nil

    macro finished
      alias HobbyType =
        \{% for hb in HOBBIES %}
          \{{ hb.type }} |
        \{% end %}
        String
    end
  end
end

struct Pizza
end

struct Sport
end

struct Movie
end

class Person
  include Model

  macro hobby(hb)
    {% HOBBIES << hb %}
    @{{ hb.var }} : {{ hb.type }}?
  end

  hobby favorite : Movie
  hobby eating : Pizza

  @name : String?

  macro finished
    def self.printype(hobby : HobbyType)
      hobby.class
    end
  end

  def hobby_types
    HobbyType
  end
end

pp Person.new.hobby_types
pp Person.printype(Movie.new)

```

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [November 20, 2022, 5:50pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/9 "2022-11-20T17:50:41Z")

</div>

That solution might work, but it’s pretty fragile. For example, `HobbyType` isn’t defined until the `finished` hooks execute. That’s a pretty strong restriction. I don’t think this is really a good idea.

I’m not familiar with the details of Granite, but I’d try to solve this by enhancing the `.create`/`#update` methods to allow types that can be converted.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [November 20, 2022, 5:54pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/10 "2022-11-20T17:54:13Z")

</div>

The implementation of `.create` is this:

```crystal
    def create(**args)
      create(args.to_h)
    end

```

I suppose it should be possible to iterate over `args` and apply defined converters on column values that need to be converted.

---

<div class="post-metadata">

**Author:** ![asterite](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/asterite/32/60_2.png) [@asterite](https://forum.crystal-lang.org/u/asterite)\
**Post date:** [November 20, 2022, 6:09pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/11 "2022-11-20T18:09:04Z")

</div>

Fragile how? What do you need the type before the finished hook? It’s the same as defining the type at the end of the program.

That said, I would always try to avoid macros or. This kind of stuff. But I guess it’s inevitable given the features the language has.

---

<div class="post-metadata">

**Author:** ![Pan](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/pan/32/1475_2.png) [@Pan](https://forum.crystal-lang.org/u/Pan)\
**Post date:** [November 21, 2022, 1:00pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/12 "2022-11-21T13:00:20Z")

</div>

Awesome! This works as I expected.

After `HobbyType` is defined, `HOBBIES` is actually not needed. `HOBBIES.compact!` can’t remove any elements. `HOBBIES=nil` also doesn’t work. So `HOBBIES[]` is still available during runtime as

```auto
Person::HOBBIES # => [nil, nil]

```

It it possible to use a temorary variable that is only used during compile-time so that it will not consume any runtime memory?

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [November 21, 2022, 1:47pm UTC](https://forum.crystal-lang.org/t/dynamic-union-type-as-a-constraint-to-class-method-argument-type/5121/13 "2022-11-21T13:47:22Z")

</div>

> [@Pan](#):
>
> It it possible to use a temorary variable that is only used during compile-time so that it will not consume any runtime memory?

If you never use `HOBBIES` in runtime code it won’t be initialized, and following that there will not be any code generated.  
So your code to print `Person::HOBBIES` only causes it to be compiled. If you’d omit that, it wouldn’t.  
You can make sure to exclude it from code generation by changing the type to `Array(_)`: `[] of _` - this will never compile, but it works for macro use.
