You have the ability to define a specific type in TypeScript:
type Email = `${string}@${string}.${string}`
This defined type offers some level of safety,
type Email = `${string}@${string}.${string}`
const ok: Email = '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7c14191010133c1b111d1510521f1311">[email protected]</a>' // okay
const drawback1: Email = '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a7cfc2cbcbc8e7c0cac6cecb8989c4c8ca">[email protected]</a>' // no error, but should have been flagged
const drawback2: Email = 'hello@@@@@@gmail..com' // no error, but should have been flagged
const fails1: Email = 'hellogmail.com' // expected error
const fails2: Email = 'hello@gmail_com' // expected error
const fails3: Email = '@gmail_com' // expected error
const fake = { address: 'noemail', validatedOn: new Date() }
type ValidatedEmail = { address: Email; validatedOn: Date }
const validateEmail = (email: string): ValidatedEmail => email as unknown as ValidatedEmail
const sendEmail = (email: ValidatedEmail) => { }
sendEmail(fake) // fails
However, there are notable drawbacks to this approach. For example, it allows input like 'hello@@@@@@gmail..com'
.
To achieve better static validation, an additional function such as validateEmail
is necessary.
It is important to note that this answer primarily focuses on static validation. For runtime email validation, refer to this answer.
In order to validate provided emails effectively, certain utilities need to be implemented. These utilities serve as context and aid in the validation process:
type Email = `${string}@${string}.${string}`
type AllowedChars =
| '='
| '+'
| '-'
| '.'
| '!'
| '#'
| '$'
| '%'
| '&'
| "'"
| '*'
| '/'
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~'
type Sign = '@'
type IsLetter<Char extends string> = Lowercase<Char> extends Uppercase<Char> ? false : true
{
type _ = IsLetter<'!'> // false
type __ = IsLetter<'a'> // true
}
type IsAllowedSpecialChar<Char extends string> = Char extends AllowedChars ? true : false
AllowedChars
denotes permissible characters before the @
symbol.
The validation process comprises three distinct states:
type FirstState = [before_sign: 1]
type SecondState = [...first_state: FirstState, before_dot: 2]
type ThirdState = [...second_state: SecondState, after_dot: 3]
FirstState
signifies the state prior to the @
symbol
SecondState
corresponds to the state after @
and before the first period .
ThirsState
represents the state following the usage of @
and .
.
These states can be viewed as context during the validation process.
Given that each state has its own set of allowed and disallowed symbols, appropriate helper functions must be created:
type IsAllowedInFirstState<Char extends string> =
IsLetter<Char> extends true
? true
: IsAllowedSpecialChar<Char> extends true
? true
: Char extends `${number}`
? true
: false
type IsAllowedInSecondState<Char extends string> =
IsLetter<Char> extends true
? true
: false
type IsAllowedInThirdState<Char extends string> =
IsLetter<Char> extends true
? true
: Char extends '.'
? true
: false
Please bear in mind that I am not an expert in email validation protocols and standards. While this type can catch numerous invalid scenarios, it may not cover all cases. Therefore, treat this type as a learning exercise rather than a production-ready tool. Rigorous testing is advised before integrating it into your codebase.
The validation utility performs nested conditional checks across individual characters, which may look complex. It evaluates the current state and character for each iteration:
type Validate<
Str extends string,
Cache extends string = '',
State extends number[] = FirstState,
PrevChar extends string = ''
> =
Str extends ''
? (Cache extends Email
? (IsLetter<PrevChar> extends true
? Cache
: 'Last character should be valid letter')
: 'Email format is wrong')
: (Str extends `${infer Char}${infer Rest}`
? (State extends FirstState
? (IsAllowedInFirstState<Char> extends true
? Validate<Rest, `${Cache}${Char}`, State, Char>
: (Char extends Sign
? (Cache extends ''
? 'Symbol [@] can\'t appear at the beginning'
: Validate<Rest, `${Cache}${Char}`, [...State, 2], Char>)
: `You are using disallowed char [${Char}] before [@] symbol`)
)
: (State extends SecondState
? (Char extends Sign
? 'You are not allowed to use more than two [@] symbols'
: (IsAllowedInSecondState<Char> extends true
? Validate<Rest, `${Cache}${Char}`, State, Char>
: (Char extends '.'
? PrevChar extends Sign ? 'Please provide valid domain name' : Validate<Rest, `${Cache}${Char}`, [...State, 3], Char>
: `You are using disallowed char [${Char}] after symbol [@] and before dot [.]`)
)
)
: (State extends ThirdState
? (IsAllowedInThirdState<Char> extends true
? Validate<Rest, `${Cache}${Char}`, State, Char>
: `You are using disallowed char [${Char}] in domain name]`)
: never)
)
)
: never)
type Ok = Validate<'<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e0cbcbcba0878d81898cce838f8d">[email protected]</a>'>
type _ = Validate<'gmail.com'> // "Email format is wrong"
type __ = Validate<'.com'> // "Email format is wrong"
type ___ = Validate<'hello@a.'> // "Last character should be valid letter"
type ____ = Validate<'hello@a'> // "Email format is wrong"
type _____ = Validate<'1@a'> // "Email format is wrong"
type ______ = Validate<'+@@a.com'> // "You are not allowed to use more than two [@] symbols"
type _______ = Validate<'john.doe@_.com'> // "You are using disallowed char [_] after symbol [@] and before dot [.]"
type ________ = Validate<'john.doe.com'> // "Email format is wrong"
type _________ = Validate<'<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="345e5b5c5a1a505b51741a575b59">[email protected]</a>'> // "Please provide valid domain name"
type __________ = Validate<'john.doe@.+'> // "Please provide valid domain name"
type ___________ = Validate<'-----@a.+'> // "You are using disallowed char [+] in domain name]"
type ____________ = Validate<'@hello.com'> // "Symbol [@] can't appear at the beginning"
function validateEmail<Str extends string>(email: Str extends Validate<Str> ? Str : Validate<Str>) {
return email
}
const result = validateEmail('@hello.com') // error
Playground
The above utility involves intricate nested conditions to determine email validity. For better readability, consider assigning types to each potential error message:
type Error_001<Char extends string> = `You are using disallowed char [${Char}] in domain name]`
type Error_002<Char extends string> = 'You are not allowed to use more than two [@] symbols'
Prior to implementation in your project, conduct extensive tests to ensure that the utility accurately validates emails without blocking legitimate ones.
The utility systematically examines each character based on the current state and transitions accordingly:
- If the character is permitted in the current state, proceed to the next character.
- If the character indicates a transition to a new state, continue processing while updating the state.