A common idiom is to use a smart constructor.
module Email (email, fromEmail, Email()) where
-- export the type, but not the constructor
newtype Email = Email String
-- export this
email :: String -> Maybe Email
email s | validEmail s = Just (Email s)
| otherwise = Nothing
-- and this
fromEmail :: Email -> String
fromEmail (Email s) = s
This will verify emails at runtime, not compile time.
For compile time verification, one would need to exploit a GADT-heavy variant of String
, or use Template Haskell (metaprogramming) to do the checks (provided the email value is a literal).
Dependent types can also ensure values are of the right form, for those languages that support them (e.g. Agda, Idris, Coq). F-star is a variant of F# that can verify preconditions/postconditions, and achieve some advanced static checks.