DEV Community

Reece Daniels
Reece Daniels

Posted on • Originally published at rubensbits.co on

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(),
}))


Enter fullscreen mode Exit fullscreen mode

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>;


Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)