Stop the instantiation of type alias

Is there a way to restrict the creation of an instance of a type alias, such as ValidatedEmail?

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

Let's say we have functions validateEmail and sendEmail.

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

It is possible to create an invalid instance of ValidatedEmail, like this:

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

Is there a method to prevent this without employing classes?

Update: The goal is to prohibit the creation of an instance of ValidatedEmail unless the email is validated. This ensures that any instance of ValidatedEmail has been properly validated. In a class-based approach, one could accomplish this by making the constructor private.

Answer №1

Is there a way to avoid this situation?

In the realm of type systems, preventing this scenario is not feasible. However, there is a straightforward solution that consistently yields results:

const imitator = corruptedInfo as unknown as AuthenticEmail;

Answer №2

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.

Answer №3

To prevent unverifiable function calls, TypeScript v5.2.2 offers a solution using generics. The code below ensures that only valid calls are made to validateEmail.

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

const validateEmail = <T extends string>(
  email: Validate<T> extends true ? T : Uninstantiable<T>,
): ValidatedEmail => {
  return { address: email, validatedOn: new Date() }
}

const sendEmail = (email: ValidatedEmail) => {}

const email1 = validateEmail("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e88e8787a88d90898598848dc68b8785">[email protected]</a>") // Works!
const email2 = validateEmail("not a valid format") // Fails!
const email3 = validateEmail("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="20464f4f604558414d504c450e434f4d">[email protected]</a>" as string) // Fails!

// Works due to a caveat: https://stackoverflow.com/a/70371008
const fake = { address: 'noemail', validatedOn: new Date() } as ValidatedEmail

// Implement additional validation here: e.g. https://stackoverflow.com/a/70206046
type Validate<T> = T extends `${string}@${string}.${string}`
  ? Lowercase<T> extends Lowercase<Uppercase<T>> ? true : false
  : false

type Uninstantiable<T> = never extends T ? Uninstantiable<T> : T

Link to Playground

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issue in TypeScript where object properties may still be considered undefined even after verifying using Object.values() for undefined values

I'm encountering an issue with TypeScript regarding my interface MentionItem. Both the id and value properties are supposed to be strings, but TypeScript is flagging them as possibly string | undefined. Interestingly, manually checking that id and va ...

Failed to decipher an ID token from firebase

I'm feeling extremely frustrated and in need of assistance. My goal is to authenticate a user using Google authentication so they can log in or sign up. Everything worked perfectly during development on localhost, but once I hosted my app, it stopped ...

Utilize an array as the response model in Amazon API Gateway using the AWS CDK

I am currently in the process of developing a TypeScript AWS CDK to set up an API Gateway along with its own Swagger documentation. One of the requirements is to create a simple endpoint that returns a list of "Supplier", but I am facing challenges in spec ...

What is the best way to retrieve a plus sign from a URL parameter?

Is there a way to include the A+ or A- bloodGroup in a URL parameter but have it display correctly? I'm trying to send it using a link like this: Can anyone help me with this issue? I need the + sign to show up properly in the response. When I use ...

Ways to extract information from an Object and save it into an array

In my Angular2 project, I am working on retrieving JSON data to get all the rooms and store them in an array. Below is the code for the RoomlistService that helps me fetch the correct JSON file: @Injectable() export class RoomlistService { constructor( ...

Is there a way to automatically scroll to the bottom of a div when it first

Looking to enhance my application with a chat feature that automatically scrolls to the bottom of the chat page to display the latest messages. Utilizing VueJs: <template> <div id="app"> <div class="comments" ...

Utilizing React-hook-Form to transfer data between two SelectBoxes

This simple logic is causing me some trouble. Despite using react-hook-form, I thought this would be easy. However, after struggling with it for over a week, I'm still facing challenges. I'm incorporating nextUI components into my project. < ...

A guide on applying color from an API response to the border-color property in an Angular application

When I fetch categoryColor from the API Response, I set border-left: 3px solid {{element.categoryColor}} in inline style. Everything is functioning correctly with no development issues; however, in Visual Studio, the file name appears red as shown in the i ...

What is the process for importing a map from an external JSON file?

I have a JSON file with the following configuration data: { "config1": { //this is like a map "a": [ "string1", "string2"], "b": [ "string1", "string2"] } } Previously, before transitioning to TypeScript, the code below worked: import ...

Encountering unexpected compilation errors in an Angular 9 project while utilizing safe null accessing and null coalescing features?

It's really strange what happened with this project. It was working perfectly fine yesterday, and I even left 'ng serve' running after finishing my work without any issues. However, today when I tried to compile the app, I ran into problems ...

Trouble arises when attempting to transfer cookies between server in Fastify and application in Svelte Kit

In the process of developing a web application, I am utilizing Fastify for the backend server and Svelte Kit for the frontend. My current challenge lies in sending cookies from the server to the client effectively. Despite configuring Fastify with the @fas ...

Error in Angular 4: Unexpected 'undefined' provided instead of a stream

I encountered an issue while attempting to make a HTTP Post request. The error message I received is as follows: auth.service.ts?c694:156 Something went wrong requesting a new password, error message: You provided 'undefined' where a stream ...

Error: 'Target is not found' during React Joyride setup

I am attempting to utilize React Joyride on a webpage that includes a modal. The modal is supposed to appear during step 3, with step 4 displaying inside the modal. However, I am encountering an issue where I receive a warning message stating "Target not m ...

Tips for adding a "Select All" feature to a dropdown list?

Currently, I have a dropdown list with a filter for IN and OUT values. The functionality is working as expected: <select class="form-select" style="max-width: 100px" [ngModel]="selectedBrand" (ngModelChange)="onChangeT ...

Facing unexpected behavior with rxjs merge in angular5

import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/merge'; this.data = this.itemsCollection.valueChanges() this.foo = this.afs.collection<Item>('products') .doc('G2loKLqNQJUQIsDmzSNahlopOyk ...

Angular StrictNullChecks: "Error - object may be null"

I am encountering an issue with the 'strictNullChecks' setting in my Angular project. This has resulted in numerous errors across my templates (.html), such as: <input #inputValue type="text" (keyup.ent ...

How to Use an Object Created from a Different Class in TypeScript

Scenario In the development process, I am using an auth.service.ts. This service is responsible for fetching user information from the database upon login. The retrieved data is then used to create a new user object. Here is a snippet of the code: user: ...

Exploring the world of tabbed dynamic routing in Angular 2 version 4

Today I encountered a routing issue that requires assistance from all of you. Currently, I have a sidebar with dynamic tree view navigations on the left and 4 tabs on the right. By default, tab1 is selected to display data related to the active link. Lin ...

Utilize the npm module directly in your codebase

I am seeking guidance on how to import the source code from vue-form-generator in order to make some modifications. As a newcomer to Node and Javascript, I am feeling quite lost. Can someone assist me with the necessary steps? Since my Vue project utilize ...

Issue with updating boolean values in reactive form controls causing functionality to not work as expected

I am currently facing an issue with validating a group of checkboxes. The main problem is that even though the 'this.dataService.minRequired' variable updates in the service, the validation state does not reflect these changes. I initially though ...