Enhanced Form Validation Using TypeScript: Frontend and Backend Strategies

Anton Ioffe - November 7th 2023 - 9 minutes read

In the ever-evolving world of web development, form validation plays a pivotal role in enhancing user experience and data integrity. However, executing this critical task with JavaScript brings forth a myriad of intricate hurdles. Embark on an insightful journey where we uncover the potent capabilities of TypeScript and its triumph over JavaScript for form validation, both on the frontend and backend. From exploring tangible strategies and effective libraries to designing articulate error messages, this article promises to equip you with advanced skills that can propel your form validation approach to new pinnacles of robustness and user experience. With a potent blend of theory and pragmatic code examples, prepare to transcend existing barriers and revolutionize your validation strategies today.

Unmasking the Challenges with JavaScript Form Validation

Handling form validation has its hurdles when using JavaScript, especially considering the inconsistencies across different browsers. Specifically, number-based input fields using HTML's input type set to "number" shows a considerable lack of uniformity across different browsers. Browsers such as Chrome and Microsoft Edge have restrictions on what can be input, ensuring the format is adhered to. However, others like Firefox and Safari have virtually no barriers, letting users input any character type into these fields. This discrepancy can cause some serious validation conflicts.

Diving into specific issues of form validation with JavaScript, let's discuss the case of number fields with invalid values. For fields with the input type="number" attribute containing invalid content, JavaScript behaves in an unexpectedly counterproductive way. Let's illustrate with an example:

let numberField = document.getElementById('numberOfItems');
console.log(numberField.value); // Returns an empty string if invalid

If the input here is determined as invalid, JavaScript returns an empty string. It doesn't bring back the erroneous input for examination or rectification. This hindrance is particularly problematic with forms requiring complex conditional validations or computations. For example, validating input fields representing someone's age or an item quantity becomes a considerable task.

Adding to these peculiarities in JavaScript validation is its approach to handling scientific notation in number type input fields. Interestingly, JavaScript allows scientific notation such as '2.3e4' (which equals 23,000 in normal decimal numeral form) to pass as valid input when using input type="number".

While this is technically valid, it may unfortunately collide with real-world form validation requirements, further complicating the validation process. To mitigate this, you can include custom validation to handle these cases, parsing numerical values using the Number() constructor in JavaScript, and further applying a regular expression check to ensure the number fits the conventional format:

let numberField = document.getElementById('numberOfItems');
let numberValue = Number(numberField.value);
if (!/^(\-|\+)?([0-9]+(\.[0-9]+)?|Infinity)$/.test(numberValue)) {
   console.log('Invalid input');
} else {
   console.log('Valid input');
}

To deal with the issue of JavaScript returning empty strings for invalid inputs, consider wrapping input retrieval in a utility function that defaults to a specific value, like so:

function getNumberInputValue(inputElement) {
    return inputElement.value || 'INVALID_INPUT';
}
let numberField = document.getElementById('numberOfItems');
console.log(getNumberInputValue(numberField)); // Returns 'INVALID_INPUT' if invalid

That way, you still have an informative return value that allows you to handle invalid inputs more appropriately, rather than be left empty-handed—quite literally.

The Comparative Advantage of TypeScript in Form Validation

The eminent advantage of TypeScript (TS) comes to the fore in form validation, thanks to its robust type-checking system. TypeScript provides an extra layer of safety, supporting the development experience with reliability and increasing productivity. Given its static type system, TypeScript ensures all code remains legal in terms of type allocation. For example, the function validateInput(someNum: number) will not compile if a string type value is passed, spot-checking possible errors:

function validateInput(someNum: number){
    // Perform some validation
}

validateInput('123'); // This will throw an error during TypeScript's compilation

A significant perk of TypeScript, 'generics', leverages type safety while advocating for code reusability. Essentially, generics allow you to write a function that accepts different types, increasing code modularity and decreasing the chances of repeat code. Here's a simple real-world code example of a function validateField that operates on diverse data types using generics, enhancing flexibility:

function validateField<T>(field: T) {
    // Perform some validation based on the type of field
}

validateField<string>('Name'); // Can be used with string
validateField<number>(123); // Can also be used with number

Transitioning to TypeScript also accelerates maintainability, moderating the bugbear of code refactoring inherent in JavaScript. This boils down to TypeScript's enforcement of function signatures, leaving JavaScript's 'one function fits all' approach in the dust. When you need to refactor a function like validAge(someNum) accepted in various application areas, TypeScript's vigilant compiler assists you during the process. It identifies all inappropriate usages before runtime, enabling code bases to stay clean and maintainable:

function validAge(someNum: number) {
    // Validate age
}

validAge('30'); // This part of code will be flagged during TypeScript's compilation

Using TypeScript, we ensure better type safety, code reusability, flexibility, and code maintainability, bestowing it with an edge over JavaScript for form validation.

Strategies for Implementing TypeScript Form Validation on the Frontend

With TypeScript at the helm, several distinct strategies can be adopted to enhance form validation on the frontend. One such strategy utilizes the power of the library class-validator. The class-validator library allows you to define validations for class members using decorators, providing a streamlined approach to validation. Consider the example below where we define a Post model:

import {IsString, IsInt, Min, Max} from 'class-validator';

export class Post {
    @IsString()
    title: string;

    @IsInt()
    @Min(0)
    @Max(10)
    rating: number;
}

The @IsString() and @IsInt() decorators enforce type validation, while @Min() and @Max() ensure the value is within the specified range.

Another strategy employs the ngx-reactive-form-class-validator, a lightweight open-source library designed specifically for enhancing validation in Angular reactive-forms. The beauty of this library lies in its seamless integration with class-validator, creating an enriched functionality. Thereby, the validation needed for each form field can be easily linked to a specific validation defined in a class.

Let's illustrate this with an example:

import {ReactiveClassFormBuilder} from 'ngx-reactive-form-class-validator';
import {Post} from './post.model';

let post: Post;
let formGroup = ReactiveClassFormBuilder.buildFormGroup(post);

The ReactiveClassFormBuilder.buildFormGroup() method integrates class-validator decorators into Angular FormControl.

Despite the advantages these libraries present, one common mistake developers make is failing to use presentation classes for validation. Keep in mind that the solution utilizing class-validator or ngx-reactive-form-class-validator does not work with interfaces. Enumerable classes should be grouped in a framework-agnostic library to avoid unnecessary dependencies.

Reflection at this point might raise questions like: Does integrating validation approaches in this way undermine the concept of separation of concerns? Or is this considered efficient code reuse? Such thought-provoking queries can guide your path to mastering TypeScript for frontend form validation.

Ensuring Backend Validation with TypeScript: An Effective Parity

In leveraging TypeScript for back-end form validation, we can augment the validation process by capitalizing on the class-validator library's robust functionalities. This library assists developers in delineating specific validations for each class member using decorators. Coupled with TypeScript, the class-validator simplifies the validation process, eradicates code duplication between the back-end and front-end, and fosters consistency in validation logic, ultimately leading to performance-enhanced applications.

Here's a mini-lesson in using the aforementioned libraries. We'll create a user profile object, applying select validation decorators from the class-validator library:

import { IsString, Length, IsEmail, IsNumber } from 'class-validator';

export class UserProfile {
    @IsString()
    @Length(1, 100)
    name: string;

    @IsEmail()
    email: string;

    @IsNumber()
    age: number;
}

In this example, the UserProfile class members are enhanced with validation rules.

To keep the effectiveness of this approach intact, ensure not to use interfaces in place of presentation classes for validation. As interfaces solely exist in TypeScript, and are subsequently omitted during transpilation to JavaScript, class-validator cannot interface with them. Instead, opt for presentation classes annotated with decorators for each member, and create robust, structured data mappings — efficient and intuitive blueprints perfect for validation purposes.

When looking for a platform that best complements TypeScript for back-end form validation, 'nestjs' readily stands out. With its seamless compatibility with class-validator, it efficiently extends validation parameters into API controllers with no more than a few lines of code, such as shown below:

import { Controller, Post, Body } from '@nestjs/common';
import { UserProfile } from './UserProfile';
import { Validate } from 'class-validator';

@Controller('users')
export class UserController {
    @Post()
    async create(@Body() newUser: UserProfile) {
        await Validate(newUser);
        // ... create user logic
    }
}

In this snippet, nestjs uses the UserProfile class, hand in hand with class-validator, to manage the POST request body in UserController.

Lastly, TypeScript opens a powerful avenue to tightly knit front-end data validation with back-end data validation, establishing a continuous line of effective practices and safeguards against invalid data. Constructing a common package that houses API definitions and validation functions can ensure a rigorous and consistent validation process, on both the back-end and front-end. This model particularly benefits projects where a single TypeScript developer is responsible for creating both front-end and back-end functionalities, as it guarantees a cohesive perspective and approach to the validation process.

Improving UI/UX Through Quality Error Messages

Form validation is quintessential in any web-based form, especially when fields are prone to human error. One aspect of form validation that often gets overlooked is the design and delivery of error messages. When done right, it aids in providing a seamless user experience, and it’s pretty effective at reducing abandonment. However, it’s commonly neglected due to the belief that there's not much to it. Contrarily, there's quite a bit to consider. Error messages should not be an afterthought - they need to be precise, helpful, and user-friendly.

Let's first consider Error Message Placement. Embedded within the layout, there are optimal locations to place error messages to maximize user awareness. A common strategy developers utilize is placing the error messages above input fields so as not to obscure the user's input or the error message itself. This strategy, coupled with other visual cues such as icons or section highlights, immediately draws awareness to erroneous input without obscuring any valuable information. Consider the use of CSS to implement this as shown below.

.error {
  color: #D8000C; 
  background-color: #FFD2D2;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 0;
  width: 100%;
}

Generic error messages with convoluted and technical jargon often prove ineffective, hence the need to Establish Stop-Words For Your Error Messages. Gov.uk provides a list of words to avoid in their interface, including words such as ‘form post error’, ‘unspecified error’, ‘error 0x0000000643’, 'forbidden', and 'illegal. The focus should be on providing a straightforward and relevant error message void of complicated words and phrases. Prioritize friendliness and clarity while avoiding the temptation of using humor.

Moreover, enhancing the user experience involves using Actionable Hints and Input Examples. Providing real-time feedback is a great way to guide the user on what exactly the problem seems to be. This includes clear guidelines of what a correct input is and can be achieved by adding a hint under the label.

if (inputField === '') {
    errorMessage = 'This field cannot be left blank. Please provide a valid input.';
}

In conclusion, error messages are a critical element of form validation that directly impacts the UI/UX of a form. By focusing on error message placement, avoiding cryptic words, and leveraging actionable hints and correct input examples, developers can significantly elevate their form validation game. This invariably enhances the user's experience while simultaneously reducing form abandonment. It also contributes to improving the tone of the design in its entirety, providing a lasting impression on the user. Do your error messages meet these criteria?

Summary

This article explores how TypeScript can enhance form validation in modern web development. It discusses the challenges of JavaScript form validation, including inconsistencies across different browsers and issues with number fields. The article then highlights the advantages of TypeScript, such as its robust type-checking system and support for code reusability. Strategies for implementing TypeScript form validation on the frontend and backend are also explored, including the use of libraries like class-validator and ngx-reactive-form-class-validator. Additionally, the article emphasizes the importance of quality error messages in improving the user experience.

Challenging task for the reader: Implement form validation using TypeScript for a complex form with conditional validations and computations, ensuring that error messages are precise, helpful, and user-friendly. Consider incorporating strategies learned from the article, such as using TypeScript's type-checking system and leveraging libraries like class-validator.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers