1

Is it possible to prevent the creation of an instance of a type alias, e.g. an ValidatedEmail?

type ValidatedEmail = { address: string; validatedOn: Date }

Lets assume having a function validateEmail and a function sendEmail.

const validateEmail = (email): ValidatedEMail => {...}
const sendEmail = (email: ValidatedEmail) => {...}

I could just create an invalid instance of ValidatedEmail like this and pass it to sendEmail:

const fake = { address: 'noemail'; validatedOn: new Date() }

Is it possible to prevent this somehow without using classes?

Update: I want to prevent that somebody can create an instance of ValidatedEmail without the email being actually validated and if you get an instance of ValidatedEmail you can be sure it has been validated. Using classes I would make the constructor private.

Alexander Zeitler
  • 11,919
  • 11
  • 81
  • 124

2 Answers2

1

Is it possible to prevent this somehow

In the type system this isn't possible. The simple recipe that will always work:

const impostor = invalidData as unknown as ValidatedEmail;
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
0

You can create this type:

type Email = `${string}@${string}.${string}`

Above type provides some safety,

type Email = `${string}@${string}.${string}`

const ok: Email = 'hello@gmail.com' // ok
const drawback1: Email = 'hello@gmail..com' // no error, but should be
const drawback2: Email = 'hello@@@@@@gmail..com' // no error, but should be


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

but, as you might have noticed, it full of drawbacks. For instance it allows you to use 'hello@@@@@@gmail..com'.

In order to do some better static validation, we need to use extra function, just like validateEmail.

Please keep in mind that my answer is fully focused on static validation. If you are interested in runtime email validation you should check this answer.

In order to validate provided email we should provide some utils. I hope they are self explanatory:

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 - represents chars which are allowed to use before @.

Our validation consists of 3 states:

type FirstState = [before_sign: 1]
type SecondState = [...first_state: FirstState, before_dot: 2]
type ThirdState = [...second_state: SecondState, after_dot: 3]

FirstState - represents state before @ symbol

SecondState - represents state after @ and before first dot .

ThirsState - represents state after using @ and dot ..

Consider above states as some sort of context. This is how I think about it.

Since each state has its own allowed and disallowed symbols, lets' create appropriate helpers:


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 keep in mind that I'm not email validator master and I'm not very well familiar with email validatio rules, protocol and standard. It means that my type can catch a lot of invalid cases but not all of them. Treat this type as a fun toy and not as a prod ready utility. This utility should be thoroughly tested before using it in your code.

Here is validation utility:

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<'+++@gmail.com'>

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<'john.doe@.com'> // "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

Above utility is just a series of nested conditional statements. It looks terrible. You can provide types for each error string, smth like this:

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'

Before using this code in your codebase, please write more tests, just to make sure that above util does not invalidates valid emails.

Above utils iterates through each char and checks current state and char.

  • If char is allowed for current state - go to the next char.
  • If char represents next state, go to the next char and in the same time go to the next state.