Type Aliases: Not useful?

I have a proposal:

I think type aliases are universally not useful. Any time I see them, I believe a custom type would serve better. I like that custom types are more powerful (can be recursive, can include multiple variants), and don’t allow you to get “stringly typed” problems through the compiler.

In this case:

addNumbers : Bool -> Bool -> Int -> Int -> Int

-- with aliasing:

type alias RoundUp = Bool
type alias RoundToTens = Bool

addNumbers : RoundUp -> RoundToTens -> Int -> Int -> Int

Is much better solved like so:

type RoundingDirection = RoundUp | RoundDown
type RoundTo = RoundOnes | RoundTens

addNumbers : RoundingDirection -> RoundTo -> Int -> Int -> Int

And this case is better solved with a new type that can’t accidentally be used in the wrong place, or perhaps a record:

type alias Name = String
type Person = Person Name

-- this is more safe:
type Name = Name String
type Person = Person Name

-- this is another good option:
type Person = Person { name : String }

Does anyone disagree with this? Would love to hear counterpoints

2 Likes

I mainly use type alias for records. Sometimes you want to hide the structure of a record but a lot of the time, you want to use it, it is part of the API.

Another case for the alias is declaring a type in terms of another type. Something like this:


type ID = ID Int 

type alias Collection a = 
    Dict Int a

get : ID -> Collection a -> Maybe a 
get (ID id) = 
    Dict.get id   
2 Likes

I think there are two competing concerns going on here:

  1. booleans are inherently unclear
  2. type aliases are sometimes unclear

For the boolean stuff, yeah: use a custom type. I’m on board with that every time. You will almost always get a clearer, safer API. Here’s a whole talk on that: Solving the Boolean Identity Crisis by Jeremy Fairbank.

As for type aliases in general, here are my rules:

  • never use type aliases for simple types like Int or Bool. They let you express intent but, crucially, are not checked by the compiler (e.g. RoundUp and RoundToTens in your example can be exchanged and will still compile.)
  • sometimes use type aliases for types with type parameters so that Result x a or Dict comparable a become HttpResponse or Students. These have a very low chance of being accidentally swapped in, and the damage is pretty low if they are. That said, I always start with a closed custom type and open it up if necessary instead of the other way around, and has no runtime cost as the compiler will unbox them if it can.
  • always use type aliases for records. That’s it.
8 Likes

I’m a huge fan of custom types! :heart_eyes:

In my own code, I follow a similar system as @brian. Here are some stats from a recent small SPA project I worked on: (~3000 line of Elm code):

There are 30 uses of type alias (vs 23 uses of type). Of those 30 aliases:

  • 26 are records
  • 2 are aliasing model to another type to reduce churn in boilerplate functions
  • 1 is a function
  • 1 is a complex dictionary type with both variables filled in.

In this codebase, type aliases are not used for safety. They aren’t primarily used to improve readability.

  1. Their main use is shortening long complex types to a short one or two word name that can more easily be used in signatures.
  2. It’s also a form of DRY. If I change the type of a record or a dictionary, I only need to update the type in a single place.

Better readability is a nice side-effect of both goals mentioned above :slightly_smiling_face:


An exception to that general rule is that I'm aliasing my top-level `Model` type:
type alias Model = Result String SomethingElse

This is so the standard signatures of update, main, view, and init can remain the same, even as my model type evolves over time.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.