All Articles

Creating Interfaces From Joi Schemas In TypeScript


Thumbs Up Image

Problem

Summary

Using joi inside a Typescript project means you need to create two validation schemas, one for Joi at runtime and one for Typescript at compile time.

This causes you issues with maintainability, consistency, and violates the DRY principle.

Code Example

import * as joi from '@hapi/joi';

// Ignore the technical arguments of these schemas - purely for example purposes
interface UserAddress = {
  // etc..
  postCode: string;
  startDate: string;
  endDate: string;
}

interface User = {
  name: string;
  phoneNumber?: number;
  addresses: UserAddress[]
}

const userAddress = joi.object({
  // etc...
  postCode: joi.string().required()
  startDate: joi.date().required(),
  endDate: joi.date().required()
})

const user = joi.object(({
  name: joi.string().required(),
  phoneNumber: joi.number().optional(),
  addresses: joi.array().items(userAddress).required(),
}))

Solution

Summary

Use joi-extract-type.

An NPM package that is surprisingly underused (currently only 4,300 downloads per week on NPM) give it’s support for complex joi schemas.

There are two issues that should be noted as of writing this; one technical, one aesthetic

  1. It has trouble managing the .unknown joi method at the object level (which declares that unknown keys are allowed on an object). Looking through the joi-extract-type code, it appears the method isn’t catered for.
  2. You lose the pretty TypeScript interface look and feel - with a lot of your code now rendered in the standard JavaScript object colouring (theme dependant of course).

Code Example

import * as joi from '@hapi/joi';
import 'joi-extract-type'

const userAddress = joi.object({
  // etc...
  postCode: joi.string().required()
  startDate: joi.date().required(),
  endDate: joi.date().required()
})

const user = joi.object(({
  name: joi.string().required(),
  phoneNumber: joi.number().optional(),
  addresses: joi.array().items(userAddress).required(),
}))

export type User = joi.extractType<typeof user>;
export type UserAddress = joi.extractType<typeof userAddress>;

User & UserAddress will work equivalent to traditionally declared User & UserAddress interfaces.

You’ll also need to make sure the version of joi-extract-type that’s being used is aligned with the correct version of joi.

@hapi/joi@17 & joi-extract-type@15.0.2 seem to work fine together.


Problem Solved

If you have any questions you’d like to ask me about this post, feel free to reach me on Twitter or Github.