TypeScript theorytheory 0/50 · 0%
Testing · hard

49. Building a mini type-safe validator (capstone groundwork)

Combining guards, generics, and unions into a small validation toolkit.

A minimal, dependency-free validator combines discriminated results, generics, and custom type guards — the same building blocks used in production libraries like zod, scaled down.

ts
type ValidationResult<T> = { valid: true; value: T } | { valid: false; errors: string[] };

interface Validator<T> {
  validate(input: unknown): ValidationResult<T>;
}

function stringValidator(): Validator<string> {
  return {
    validate(input) {
      return typeof input === "string"
        ? { valid: true, value: input }
        : { valid: false, errors: ["expected a string"] };
    },
  };
}

function objectValidator<T extends Record<string, unknown>>(
  shape: { [K in keyof T]: Validator<T[K]> }
): Validator<T> {
  return {
    validate(input) {
      if (typeof input !== "object" || input === null) return { valid: false, errors: ["expected an object"] };
      const errors: string[] = [];
      const result: Partial<T> = {};
      for (const key in shape) {
        const r = shape[key].validate((input as Record<string, unknown>)[key]);
        if (r.valid) result[key] = r.value; else errors.push(`${key}: ${r.errors.join(", ")}`);
      }
      return errors.length ? { valid: false, errors } : { valid: true, value: result as T };
    },
  };
}

`objectValidator`'s mapped-type parameter (`{ [K in keyof T]: Validator<T[K]> }`) ties each field's validator to the corresponding field's type in `T`, so mismatched validators are caught at compile time, not just at runtime.

Check your understanding

  1. 1. What does `ValidationResult<T>` model?

  2. 2. What does the shape parameter's type `{ [K in keyof T]: Validator<T[K]> }` enforce?

  3. 3. What real-world libraries does this pattern resemble?