Skip to content
Type Aliases and newtype
step 1/7

Reading — step 1 of 7

Learn

~3 min readCustom Types

Haskell has THREE ways to introduce a name for a type. Each has different semantics. The Haskell wiki and Real World Haskell cover them; modern code uses all three.

type — synonyms

type Name = String
type Age = Int
type Email = String

greet :: Name -> String
greet n = "Hello, " ++ n

type creates an ALIAS — Name is just String. Same runtime, same operations. Just a documentation convenience.

fn :: String -> Int        -- equivalent to:
fn :: Name -> Age

-- Both compile and are interchangeable.

Useful for documentation but no type safety — you can pass a String where Name is expected because they're the SAME TYPE.

data — new type with constructors

data Email = Email String      -- Email is a new TYPE; Email (the constructor) wraps a String

greet :: Email -> String
greet (Email addr) = "Email: " ++ addr

x = Email "[email protected]"
-- y = "[email protected]" :: Email     -- error: not an Email

data creates a TRULY NEW type. The compiler enforces the distinction.

newtype — single-constructor data with no overhead

newtype Email = Email { unEmail :: String }
newtype Age = Age Int

newtype is like data BUT:

  • Restricted to ONE constructor with ONE field
  • The wrapper is ERASED at runtime — same memory layout as the underlying type
  • Provides type safety with zero runtime cost
greet :: Email -> String
greet (Email addr) = "Email: " ++ addr

-- compile-time distinction:
greet "plain string"           -- ERROR
greet (Email "[email protected]")      -- OK

When to use which

NeedUse
Documentation, no enforcementtype
Genuine new type with multiple constructors or multiple fieldsdata
Type-safe wrapper around one existing type, zero runtime costnewtype

Examples:

  • type Name = String — just a hint
  • data Tree a = Leaf | Node a (Tree a) (Tree a) — many constructors
  • newtype UserId = UserId Int — distinguish from Int, no overhead

Why newtype matters in practice

newtype Pixel = Pixel Int
newtype Inch = Inch Int

addPixels :: Pixel -> Pixel -> Pixel
addPixels (Pixel a) (Pixel b) = Pixel (a + b)

-- Now mixing units is a type error:
let p = Pixel 100
let i = Inch 5
-- addPixels p i             -- TYPE ERROR
addPixels p (Pixel 50)         -- OK

Classic "units of measure" pattern. The Pixel and Inch newtypes prevent mixing — the compiler catches Mars Climate Orbiter-style bugs at compile time.

Popular libraries like tagged and refined build on this idea for typed validation.

newtype with deriving

newtype Email = Email String
    deriving (Show, Eq, Ord)

newtype Age = Age Int
    deriving (Show, Eq, Ord, Num)        -- can derive Num via GeneralizedNewtypeDeriving

The Num derive (with the GeneralizedNewtypeDeriving extension) lets Age behave numerically — Age 5 + Age 3 = Age 8 — while staying distinct from Int.

Deriving via and stock

Modern Haskell has more deriving strategies (GHC 8.2+):

newtype EmailNT = EmailNT String
    deriving stock (Show, Eq)         -- stock: built-in derivation
    deriving newtype (Ord)             -- newtype: reuse the wrapped type's instance
    -- deriving via Validated EmailFormat (Validate)   -- via: use a different type's instance

For advanced cases. Day-to-day code uses plain deriving.

Common mistakes

  • Reaching for data when newtype fits — newtype is faster (no runtime overhead) and safer.
  • Reaching for type when you want type safety — type is just a synonym; doesn't prevent mixing.
  • Trying to use newtype with multiple fields or constructors — must use data.
  • Using newtype-wrapped types in error messages — they appear with the wrapper. Pixel 100 not 100. Sometimes desired, sometimes annoying.
  • Forgetting accessor namesnewtype Email = Email { unEmail :: String } gives you unEmail :: Email -> String. Without the accessor, you must pattern match.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…