Skip to content
Newtype and Type Wrappers
step 1/5

Reading — step 1 of 5

Learn

~1 min readType-Level Programming

newtype wraps a type with a different name — at zero runtime cost. Used for: documentation, smart constructors, multiple instance choices.

newtype Age = Age Int           -- now Age is its own type, but compiles to Int
newtype UserId = UserId Int

function sendEmail :: UserId -> Email -> IO ()
function registerUser :: Age -> Email -> IO UserId

-- Compile error: can't pass UserId where Age expected, even though both are Int

The wrapper is erased at compile timenewtype has no runtime overhead, unlike data (which is a real wrapper).

Smart constructors — hide the constructor:

module MyModule (Email, mkEmail, emailValue) where

newtype Email = Email String         -- constructor not exported

mkEmail :: String -> Maybe Email
mkEmail s = if '@' `elem` s then Just (Email s) else Nothing

emailValue :: Email -> String
emailValue (Email s) = s

Users can't construct an Email without going through mkEmail — guarantees validity.

Multiple type class instances — the killer use case:

-- The Sum/Product newtypes pick which monoid Int uses:
import Data.Monoid

getSum (Sum 3 <> Sum 4)         -- 7   (addition)
getProduct (Product 3 <> Product 4)   -- 12  (multiplication)

Int is a Monoid in two ways (sum, product). Without newtypes, you couldn't define both — the compiler picks one. With newtypes, each wrapper picks one.

Similar pattern: First/Last for Maybe (pick first / last non-Nothing).

Deriving via:

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

Gets the underlying type's instances for free.

Compare to data:

data Email = Email String      -- works, but: extra runtime indirection
newtype Email = Email String   -- preferred for single-field wrappers

If the wrapper has exactly one field, prefer newtype. Multiple fields require data.

Discussion

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

Sign in to post a comment or reply.

Loading…