How to implement object validation using TypeScript?
Asked Answered
O

6

12

I have an Express API server app and a React client app both implemented in TypeScript. I defined my data models using TypeScript interfaces, and I'm using those interfaces on both ends of the system. However, TypeScript interfaces are compile-time feature only, and I need also runtime type checking, e.g. validating that HTTP POST data (json) conforms to the defined data structure.

So my question is, how could/should I implement runtime object validation utilizing the features provided by TypeScript?

Osrock answered 1/6, 2017 at 2:10 Comment(7)
You can use a npm module like revalidator to validate your dataOverpowering
Possible duplicate of Check if an object implements an interface at runtime with TypeScriptSeaport
@4castle No it is not duplicate. I asked whether TypeScripts interfaces can be utilized also in runtime (I know that by default they can't by themselves), and if not, how to utilize (other) features of TypeScript for implementing validation. There are billions of people saying that TypeScript interfaces are compile-time only, which I already know as written in the question.Osrock
@BlazeSahlzen How can I utilize that without having to define my models twice: once for compile-time (TS interfaces) and once for runtime (JSON schema)?Osrock
TypeScript has no other features for implementing validation. It is strictly a JavaScript transpiler and nothing more. The duplicate question explains this and why.Seaport
It is not be possible to use the typescript interface with revalidator, as it has it's own schema structure which is completely different. (unless you were to write a plugin to convert the typescript interfaces to revalidator schema)Overpowering
I have actually the data structure defined also in OpenAPI 2.0 spec (compatible with JSON Schema). So I guess the process would be like this: validate the HTTP payload using JSON Schema model definition, attach the appropriate TypeScript interface to the payload object after it has been successfully validated. I guess it is impossible then to avoid having two sets of definitions.Osrock
S
3

This question is old, but I'd like to share my validation library also.

It's type-script friendly, tiny (no tons of unnecessary functionality) and easily extensible by custom validators.

npm: https://www.npmjs.com/package/checkeasy

github: https://github.com/smbwain/checkeasy

import {alternatives, arrayOf, int, object, oneOf, optional, string} from 'checkeasy';

const myValidator = object({
   a: int({max: 5}),
   b: string(),
   c: optional(float()),
   d: oneOf(['a', 'b', 7] as const),
   e: alternatives([string(), int()]),
   f: arrayOf(string()),
   g: object({
       subP: string(),
   }),
});

const value = myValidator(anyUnsafeData, 'name');
// type of value is: {
//    a: number,
//    b: string,
//    c: number | undefined,
//    d: "a" | "b" | 7,
//    e: string | number,
//    f: string[],
//    g: {subP: string},
//}

It also throws clear human readable messages in errors. E.g.

myValidator({a: 'hello'}, 'data');
// throws: [data.a] should be an integer

myValidator({a: 1, b: 'string', d: 'a', e: true}, 'data');
// throws: All alternatives failed for [data.e]:
//      [data.e.@alternative(0)] should be a string
//      [data.e.@alternative(1)] should be an integer
Snaggletooth answered 22/10, 2021 at 23:14 Comment(2)
this looks interesting but I don't think works with typescript well. object and string are types in typescript, and that would conflict.Rafael
@GarrGodfrey It doesn't conflict. Typescript expects name of the type in particular places only, allowing variables to have the same names. Anyway, if you don't like it, you can import it with other name, or as wildcard into separate object (import * as types from 'checkeasy')Snaggletooth
R
2

I've created a super lightweight library called Smoke Screen which does exactly that. It leverages typescript features to perform any kind of object validation within javascript runtime. It's not 100% seamless due to the fact that javascript does not hold any type information at runtime, but thanks to TypeScript decorators, this may be easily done:

class Person {

    @exposed({type: Number})
    age: number;

}

// serialize a Person object into a JSON string
const person = new Person();
person.age = 56.8;
const smokeScreen = new SmokeScreen();
smokeScreen.toJSON(person); // -> '{"age":56.8}'

// deserialize a JSON string into a Person object
let json = JSON.stringify({age: 19});
const person2 = smokeScreen.fromJSON(json, Person);
console.log(person2); // -> Person { age: 19 }

// typing validation
json = JSON.stringify({age: "oops"});
smokeScreen.fromJSON(json, Person); // Error: illegal input - property 'age' must be a number

Additional custom validators may be set, optional parameters and null checking are also supported and enforced. I suggest reading more about it and trying it out.

Revisal answered 24/3, 2018 at 15:18 Comment(2)
Is it possible to just 'read' the TypeScript though?Intendment
You mean using it from plain javascript?Revisal
P
1

You can try out a framework/library called ts.validator.fluent. Generic object validation. Fluent rules.

https://github.com/VeritasSoftware/ts.validator

NPM Package:

https://www.npmjs.com/package/ts.validator.fluent

Here is an example of how your TypeScript models can be validated using the framework:

/* Install npm package ts.validator.fluent and then import like below */
import { IValidator, Validator, ValidationResult } from 'ts.validator.fluent/dist';

/*TypeScript model*/
class Person {
   Name: string;
}

/* Validation rules */
var validatePersonRules = (validator: IValidator<Person>) : ValidationResult => {
  return validator
             .NotEmpty(m => m.Name, "Name cannot be empty")
        .ToResult();
};

/* Populate model */
var person = new Person();
person.Name = "Shane";

/* Validate model */
/* Sync */
var validationResult = new Validator(person).Validate(validatePersonRules); 
/* Async */
var validationResult = await new Validator(person).ValidateAsync(validatePersonRules);
Pichardo answered 3/6, 2018 at 0:6 Comment(0)
K
1

I also didn't find any good solution for TypeScrpit which is:

  1. Simple to use
  2. Reusable with my existing code
  3. Share the same interfaces and logic as TypeScript

So I created a library called computed-types:

const UserSchema = Schema({
  name: string,
  amount: number,
  flags: array.of(string).optional();
});

type User = Type<typeof UserSchema>;

Equivalent code in Joi:

const UserSchema = Joi.object({
  name: Joi.string().required(),
  amount: Joi.number().required(),
  flags: Joi.array().items(Joi.string()),
});

type User = {
  name: string;
  amount: number;
  flags?: string[];
}

See more details and examples here: https://github.com/neuledge/computed-types

Koehler answered 23/6, 2020 at 18:53 Comment(3)
You're defining the schema twice though :-/Intendment
@OliverDixon Check again, you define it twice with Joi, in computed-types it's only once.Koehler
yes twice; you can do it once with TypeScript.Intendment
M
1

JavaScript: Ajv (json-schema)

I'd say for now the accepted standard definitely seems to be Ajv. It has 78M weekly downloads at the time of writing (2024).

It uses json-schema (a widely accepted standard for API constracts, testing and validations) to validate objects.

https://github.com/ajv-validator/ajv

TypeScript: typescript-json-schema

You could use typescript-json-schema (270k weekly downloads) to automatically generate json-schemas from your types.

Ajv (see above) will be able to use those json-schemas to validate in runtime.

Example

Given the following types

import type { FieldValue } from '@firebase/firestore'

export type Priority = 'low' | 'medium' | 'high'
export type Status = 'open' | 'closed'

export interface TicketData {
  id: string
  userId: string
  userEmail: string
  title: string
  body: string
  priority: Priority
  status: Status
  createdAt: FieldValue
  updatedAt: FieldValue
}

export type CreateTicketData = Pick<TicketData, 'title' | 'body' | 'priority'>

You can use typescript-json-schema to create a JSON file for your type like so (explained here):

typescript-json-schema src/data/schema/TicketData.ts CreateTicketData > src/data/schema/CreateTicketData.json

The result would be CreateTicketData.json

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "definitions": {
        "Priority": {
            "enum": [
                "high",
                "low",
                "medium"
            ],
            "type": "string"
        }
    },
    "properties": {
        "body": {
            "type": "string"
        },
        "priority": {
            "$ref": "#/definitions/Priority"
        },
        "title": {
            "type": "string"
        }
    },
    "type": "object"
}

You can now use that json file in your backend code like so:

import Ajv from 'ajv'
import schema from '@/data/schema/CreateTicketData.json'

export async function POST(request: Request) {
  try {
    // Verify request
    const { token, ticketData } = await request.json()
    await admin.auth().verifyIdToken(token)

    // Validate data
    const validate = new Ajv({ allErrors: true }).compile(schema)
    if (!validate(ticketData)) return Response.json({ error: validate.errors }, { status: 400 })

    // Create ticket and send 201 'Created' response.
    const persistedTicket = someLogic(ticketData)
    return Response.json(persistedTicket, { status: 201 })
  } catch (error) {
    return Response.json(
      { error: 'Unauthorized', reason: isDev ? error : 'Something went wrong' },
      { status: 401 },
    )
  }
}

Alternatives

Municipality answered 9/7, 2024 at 0:52 Comment(1)
Adding this answer based on industry standards, as opposed to people having written their own libs and posting it as an answer, respectfully.Municipality
R
0

I realize this question is old, but I just wrote my own validator for JSON objects and typescript, for this exact purpose, using decorators. Available here: ts-json-object

Example:

import {JSONObject,required,optional,lt,gte} from 'ts-json-object'

class Person extends JSONObject {
    @required // required
    name: string
    @optional // optional!
    @lt(150) // less than 150
    @gte(0) // Greater or equal to 0
    age?: number
}

let person = new Person({
 name: 'Joe'
}) // Ok
let person = new Person({
}) // Will throw a TypeError, because name is required
let person = new Person({
 name: 123
}) // Will throw a TypeError, because name must be a string

Has many other features such as custom validations, etc.

Rumney answered 9/3, 2020 at 21:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.