typescript 49 lines · 9 steps

How a typed signup validator collects errors

A single function checks every signup field and returns a typed map of field-specific error messages.

Explained by highlit
1type SignupInput = {
2 email: string;
3 password: string;
4 confirmPassword: string;
5 username: string;
6 age: string;
7};
8 
9type FieldErrors = Partial<Record<keyof SignupInput, string>>;
10 
11const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12const USERNAME_RE = /^[a-zA-Z0-9_]+$/;
13 
14export function validateSignup(input: SignupInput): FieldErrors {
15 const errors: FieldErrors = {};
16 
17 const email = input.email.trim();
18 if (!email) {
19 errors.email = "Email is required";
20 } else if (!EMAIL_RE.test(email)) {
21 errors.email = "Enter a valid email address";
22 }
23 
24 if (!input.password) {
25 errors.password = "Password is required";
26 } else if (input.password.length < 8) {
27 errors.password = "Password must be at least 8 characters";
28 } else if (!/[0-9]/.test(input.password) || !/[a-zA-Z]/.test(input.password)) {
29 errors.password = "Password must contain letters and numbers";
30 }
31 
32 if (input.confirmPassword !== input.password) {
33 errors.confirmPassword = "Passwords do not match";
34 }
35 
36 const username = input.username.trim();
37 if (username.length < 3) {
38 errors.username = "Username must be at least 3 characters";
39 } else if (!USERNAME_RE.test(username)) {
40 errors.username = "Username may only contain letters, numbers, and underscores";
41 }
42 
43 const age = Number(input.age);
44 if (!Number.isInteger(age) || age < 13) {
45 errors.age = "You must be at least 13 years old";
46 }
47 
48 return errors;
49}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Deriving the error shape from the input type with keyof keeps validation in sync as fields change.
  2. 2Accumulating errors into one object lets you report every problem at once instead of failing on the first.
  3. 3Ordering checks from required to format avoids running stricter rules on missing or empty values.

Related explainers

typescript
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';

Wiring validation and Swagger docs in NestJS

validation openapi decorators
Intermediate 8 steps
ruby
class TemplateInterpolator
  PLACEHOLDER = /\{\{\s*([\w.]+)\s*\}\}/
 
  def initialize(strict: false)

Interpolating templates with dotted keys in Ruby

regex string-interpolation hash-traversal
Intermediate 6 steps
typescript
import { Component, Input } from '@angular/core';
 
interface Order {
  id: string;

How Angular ICU plurals localize an order summary

i18n pluralization standalone-component
Intermediate 8 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, catchError, concatMap, finalize } from 'rxjs';
import { DataSource, QueryRunner } from 'typeorm';
 

Wrapping requests in a transaction with NestJS

interceptors transactions rxjs
Advanced 7 steps
typescript
type CsvColumn<T> = {
  header: string;
  value: (row: T) => string | number | boolean | null | undefined;
};

Building a type-safe CSV writer in TypeScript

generics serialization escaping
Intermediate 7 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js';
 
export interface NormalizedPhone {
  e164: string;

Normalizing phone numbers to E.164 in TypeScript

validation normalization error-handling
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

How a typed signup validator collects errors — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code