Introduction
These notes should help in better understanding TypeScript
and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples in this post are based on TypeScript 3.7.2.
Basics
When building applications, we often have to deal with some user provided input. A common way is to display a form, so that the user can input the data. This data then has to be validated and some feedback should be provided, incase the data is invalid. To achieve this, we validate the data and then display feedback like an error message or multiple messages.
In this post, we will write a small validation library and leverage TypeScript to improve the developer experience.
Our validation library should be framework independent and only take care of validating data, leaving the actual UI representation in user land.
Let's assume that we are provided with some user data object. This could be similar to the following example.
const fieldValues = {
name: "Test User",
level: 10,
description: "Test Description"
};
There is a specification that we need to implement, which specifies that name
should not be empty and description
should have a minimum length of 10 characters.
What we also want to do is return a validation result object, which we can pass to a UI function and render the error messages incase the data has invalid fields. This might look something like the next example.
const result = {
name: true,
level: true,
description: "Minimum of 10 characters required!"
};
The result shape might differ from case to case. Our validation library should have no assumptions on how the result is structured, except that we're returning an object. In this specific case we want a function that enables us to pass in a collection of validation rules and the previously defined fieldValues
and get back a validation result containing either a true
, when valid, or an error message
string.
const result = validate(validationRules, fieldValues);
Now that we have a general idea of how our library should function from an API
perspective, the next step is to implement that library and provide the needed functionality.
Implementation
Before we start implementing the validation library, let's recap on the form field values, which we defined as being represented as an object
.
Our validate
function should return all or a subset of the provided keys with their corresponding validation result. For our basic implementation, we assume that our field validation functions either return a true
or an error message string
.
type ValidationResult<T, U> = Partial<{ [Key in keyof T]: U }>;
The ValidationResult
type is what our validate
function will return. What's still missing is how the validation rules should be defined. To keep as much as possible in user land, our validate
function accepts a list of rules. These rules expect the complete field object and then return a validation result containing the complete object or only a subset of it.
This is how we will define a rule.
type Validation<T, U> = (fields: T) => ValidationResult<T, U>;
Let's write one or two validation functions next.
const hasLength = <T>(len: number, input: string | Array<T>) =>
input.length >= len;
Our hasLength
function expects some string or an array and then checks if the provided input is larger or equals the provided minimum length. This newly created predicate function can be a basic building for writing some validation functions.
const hasUserName = (input: string) =>
hasLength(1, input) ? true : "Name is required.";
const hasValidDescription = (input: string) =>
hasLength(10, input)
? true
: "Description requires a minimum of 10 characters.";
Next we could define a collection of validation functions to run against some provided input.
const fieldValues = {
name: "Test User",
level: 10,
description: "Test Description"
};
type FieldValues = typeof fieldValues;
/*
type FieldValues = {
name: string;
level: number;
description: string;
}
*/
Our validate
library should be able to accept and handle a collection of rules and some input and run these validations against the provided input and return a user land defined result shape.
const validationRules = [
({ name }: FieldValues) => ({
name: hasUserName(name)
}),
({ description }: FieldValues) => ({
description: hasValidDescription(description)
})
];
Now that we defined our validation rules we want to run them against our validate
function. Technically we want to iterate over all the predicate functions, collect the results and then merge them together into an object, as defined via the ValidationResult
type. To recall this is how we defined this:
type ValidationResult<T, U> = Partial<{ [Key in keyof T]: U }>;
Our implementation can be a combination of map and reduce, where we map over the predicates and then merge them into an object.
const validate = <T, U = boolean | string>(
validations: Validation<T, U>[],
fields: T
): ValidationResult<T, U> =>
validations
.map(validation => validation(fields))
.reduce((acc, a) => Object.assign(acc, a), {});
We could simplify the code a little more, by running the predicate function inside the reduce as well.
const validate = <T, U = boolean | string>(
validations: Validation<T, U>[],
fields: T
): ValidationResult<T, U> =>
validations.reduce(
(acc, validation) => Object.assign(acc, validation(fields)),
{}
);
Finally we can test this out with some data.
const fieldValues = {
name: "Test User",
level: 10,
description: "Test Description"
};
type FieldValues = typeof fieldValues;
const validationRules = [
({ name }: FieldValues) => ({
name: hasUserName(name)
}),
({ description }: FieldValues) => ({
description: hasValidDescription(description)
})
];
validate(validationRules, fieldValues); // {name: true, description: true}
In the above example both fields are valid, now let's test this with some invalid data and check the result.
const fieldValues = {
name: "Test User",
level: 10,
description: "Test"
};
validate(validationRules, fieldValues);
/*
{
description: "Description requires a minimum of 10 characters.",
name: true
};
*/
The provided description
didn't match the expected rule and our validate
functionality correctly returned the pre-defined error message.
After going through this short lesson, we should have a good idea how to leverage TypeScript when building small libraries, especially when thinking about the shape of the in/out data.
If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif
Top comments (2)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.