Is it feasible to verify the accuracy of the return type of a generic function in Typescript?

Is there a way to validate the result of JSON.parse for different possible types? In the APIs I'm working on, various Json fields from the database need to have specific structures. I want to check if a certain JsonValue returned from the database is coherent with a given type.

I've experimented with different approaches. Here is how I defined the generic function:

parseJsonField<T>(jsonValue: Prisma.JsonValue): T {
    return JSON.parse(JSON.stringify(jsonValue));
}

I am looking for a solution that throws an error when the jsonValue does not match the exact properties of type T, but this approach is not successful.

I am testing using this class:

export class ProgramTagsDTO {
  key: string;
  value: string;

  constructor(key: string, value: string) {
    this.key = key;
    this.value = value;
  }
}

Running the following test:

it('should validate', () => {
  const tags: Prisma.JsonValue = [
    {
      key: 'key1',
      value: 'value1',
      wrongField: 'value',
    },
    {
      key: 'key2',
      value: 'value2',
    },
  ];
  let result = null;
  const expectedResult = [
    new ProgramTagsDTO('key1', 'value1'),
    new ProgramTagsDTO('key2', 'value2'),
  ];
  try {
    result = programmesService.parseJsonField<ProgramTagsDTO[]>(tags);
  } catch (error) {
    expect(error).toBeInstanceOf(Error);
  }
  expect(result).toEqual(expectedResult);
});

The current outcome is:

 expect(received).toEqual(expected) // deep equality

    - Expected  - 2
    + Received  + 3

      Array [
    -   ProgramTagsDTO {
    +   Object {
          "key": "key1",
          "value": "value1",
    +     "wrongField": "value",
        },
    -   ProgramTagsDTO {
    +   Object {
          "key": "key2",
          "value": "value2",
        },
      ]

However, I want the method to throw an exception instead. The expect.toEqual is used just for logging purposes.

Answer №1

My coworker and I brainstormed for a few days and finally devised a solution that seems to be functioning well at the moment:

We included this import in our code:

import { ClassConstructor, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';

We created two methods, one for arrays and another for single values:

public static async parseJsonArray<T>(
  cls: ClassConstructor<T>,
  json: Prisma.JsonValue,
): Promise<T[]> {
  if (!Array.isArray(json)) {
    throw new UnprocessableEntityException(
      `Json value is not a ${cls.name}[]`,
    );
  }
  const res: Promise<T>[] = [];
  for (const element of json) {
    res.push(this.parseJsonValue<T>(cls, element));
  }
  return Promise.all(res);
}

public static async parseJsonValue<T>(
  cls: ClassConstructor<T>,
  json: Prisma.JsonValue,
): Promise<T> {
  const parsed = JSON.parse(JSON.stringify(json));
  const transformed: T = plainToClass(cls, parsed, {
    excludeExtraneousValues: true,
  });
  return validate(transformed as object).then((errors) => {
    if (errors.length) {
      throw new UnprocessableEntityException(
        `Json value is not a ${cls.name}, Errors: ${errors}`,
      );
    }
    return transformed;
  });
}

Although plainToClass can handle arrays, we needed a more versatile approach that can clearly indicate whether the result will be an array or a single object based on the input being parsed. This limitation prevented us from creating a single method with a return signature like T | T[].

Answer №2

Regrettably, it's not possible to retain generics in compiled code. This is a common issue across languages like Java, where generics are stripped during compilation. In the case of Typescript, things get even trickier as the 'types' are also removed, resulting in transpilation to weakly-typed Javascript.

As a result, when the following code:

parseJsonField<T>(jsonValue: Prisma.JsonValue): T {
    return JSON.parse(JSON.stringify(jsonValue));
}

is executed at runtime, it simplifies to:

parseJsonField(jsonValue) {
    return JSON.parse(JSON.stringify(jsonValue));
}

This means that there is no type information available for validation.

Additionally, if you're working with classes, it's important to note that even if you attempt to validate that the returned value aligns with the interface of T, it won't be an instance of T. Instead, it will merely be a generic object coincidentally sharing property names with an instance of T.

To address this, runtime checks need to be implemented. Building on the provided solution, you can create a more generalized approach:

// Retrieves a value and removes it from the json object
function getDelete<T>(jsonValue: any, property: string, required = false): T {
  const ret = jsonValue[property];
  delete jsonValue[property];
  if (required && typeof ret === 'undefined') {
    throw new Error(`Property ${property} not found`);
  }
  return ret;
}

// Validates that the json object is empty to avoid extraneous values
function checkEmpty(jsonValue: any) {
  if (Object.keys(jsonValue).length > 0) {
    throw new Error('Object contains extraneous properties ' + JSON.stringify(jsonValue));
  }
}

// Example of a class
public class Person {
  private firstName: string;
  private lastName: string;

  constructor(jsonValue?: any) {
    if (jsonValue) {
      // Parsing the json object into the class structure and verifying it
      this.firstName = getDelete(jsonValue, 'firstName');
      this.lastName = getDelete(jsonValue, 'lastName');
    }
    checkEmpty(jsonValue);
  }
}

function parseAndCheck<T>(jsonValue: any, constructor: (...arg: any[]) => T): T {
  return constructor(JSON.parse(JSON.stringify(jsonValue)));
}

(Note: The above code is untested and serves as a theoretical example)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The NetSuite https.post() method is throwing an error that reads "Encountered unexpected character while parsing value: S. Path '', line 0, position 0"

I'm currently facing an issue when trying to send the JSON data request below to a 3rd party system using the "N/https" modules https.post() method. Upon sending the request, I receive a Response Code of "200" along with the Error Message "Unexpected ...

Utilize interface as a field type within a mongoose Schema

I am currently working with typescript and mongoose. I have defined an interface like this: interface Task { taskid: Boolean; description: Boolean; } My goal is to create a schema where one of the fields contains an array of Tasks: const employeeSche ...

Guide on adding up the value of a property within an array of objects using AngularJS

I am receiving an array of results from a Node.js API in my Angular app, and here is how it looks: <div *ngFor="let result of results"> <div *ngIf="result.harmattan.length > 0"> ... </div> <br> .. ...

Eliminate any spaces from the JSON string's "outside" keys and values

I have a JSON string saved in a database field that looks like this: {"name" : "John Paul Mark", "surname" : "Johnson"} It's important to note that the name consists of three separate names with spaces between them. I am looking to remove the space ...

The object is classified as 'undetermined' (2571) upon implementation of map() function

Despite conducting a thorough search about this error online, I still haven't been able to find a solution. Let's jump into an example with data that looks like this: const earthData = { distanceFromSun: 149280000, continents: { asia: {a ...

Passing a Typescript object as a parameter in a function call

modifications: { babelSetup?: TransformationModifier<babel.Configuration>, } = {} While examining some code in a React project, I came across the above snippet that is passed as an argument to a function. As far as I can tell, the modifications p ...

Encountered the "Error TS2300: Duplicate identifier 'Account'" issue following the upgrade to Typescript version 2.9.1

Since upgrading to Typescript 2.9.1 (from 2.8), I encountered a compile error: node_modules/typescript/lib/lib.es2017.full.d.ts:33:11 - error TS2300: Duplicate identifier 'Account'. This issue never occurred when I was using typescript 2.7 and ...

Generate a new data structure by pairing keys with corresponding values from an existing

Imagine having a type named Foo type Foo = { a: string; b: number; c: boolean; } Now, I am looking to create a type for an object containing a key and a value of a designated type T. The goal is for the value's type to be automatically determin ...

Is TypeScript being converted to JavaScript with both files in the same directory?

As I begin my journey with TypeScript in my new Angular project, I find myself pondering the best approach for organizing all these JS and TS files. Currently, it appears that the transpiler is placing the .js files in the same directory as the correspondi ...

Returns false: CanActivate Observable detects a delay during service validation

Issue with Route Guard in Angular Application: I encountered an issue with my route guard in my Angular application. The problem arises when the guard is active and runs a check by calling a service to retrieve a value. This value is then mapped to true or ...

Filtering a multi-dimensional array in Ionic 3

I attempted to filter an array from a JSON with the following structure {ID: "2031", title: "title 1", image: "http://wwwsite.com/im.jpg", url: "url...", Goal: "3000000", …} The array is named 'loadedprojects' and below is the filteri ...

XPages: create JSON data on the server, utilize on the client-side

Struggling to overcome this seemingly simple hurdle, I find myself puzzled on how to proceed. My goal is to create a JSON structure on the server using ArrayObject and ObjectObject objects, and utilize it as a datasource both on the server-side (which work ...

What is the method for defining specific requirements for a generic type's implementation?

I am facing an issue with the following code snippet, where I am trying to restrict the pairing of Chart objects based on correct types for the data and options objects. However, despite my efforts, the TypeScript compiler is not throwing an error in the s ...

The 'flatMap' property is not found on the 'string[]' data type. This issue is not related to ES2019

A StackBlitz example that I have set up is failing to compile due to the usage of flatMap. The error message reads: Property 'flatMap' does not exist on type 'string[]'. Do you need to change your target library? Try changing the ' ...

Extract data from arrays within other arrays

I have an array called societies: List<Society> societies = new ArrayList<>(); This array contains the following information: [{"society_id":1,"name":"TestName1","email":"Test@email1","description":"TestDes‌​1"}, {"society_id":2,"name":" ...

Error: The absence of an element identified by the locator does not cause the protractor spec to fail, but rather it executes successfully

This automation framework follows the page object model and utilizes the async/await approach rather than promises. TypeScript is used, with compilation to JavaScript (protractor) for script execution. Page Object: async addProjectDetails(): Promise< ...

show image data from json source

Hey there! I am currently sending a JSON to JSP that contains a file URL. My goal is to display an image using this URL in AJAX. Can anyone assist me with this? //controller recordingInfo = recordingManager.getRecordingInfo(new Integer(recordingId)); ...

Exploring JSON objects with nested structures using Jsonpath queries

I have a JSON dataset that I am trying to query for a specific value: http://pastebin.com/Vf59Cf9Q The goal is to locate the description == "chassis" within this path: $.entries..nestedStats.entries..nestedStats.entries.type Unfortunately, I am struggl ...

TypeScript's robustly-typed rest parameters

Is there a way to properly define dynamic strongly typed rest parameters using TypeScript 3.2? Let's consider the following scenario: function execute<T, Params extends ICommandParametersMapping, Command extends keyof Params, Args extends Params[C ...