Is there a way to recursively convert property types from one to another in an object that contains optional properties?

The scenario: I am currently working with MongoDB and a REST API that communicates using JSON. MongoDB uses objects instead of identifiers for documents, but when these objects are stringified (such as in a response body), they get converted into strings.

The following example is quite condensed, but it serves the purpose of illustrating my question.

// This function should recursively convert all ObjectId types to string types
type JsonDocument<D> = {
    [K in keyof D]: D[K] extends ObjectId ? string : D[K] extends object ? JsonDocument<D[K]> : D[K];
};

// The document type structure in the database
type FooDocument = {
    _id: ObjectId;
    updatedBy?: ObjectId;
};

// The document type structure in a response body
type FooJsonDocument = JsonDocument<FooDocument>;

// The expected structure of FooJsonDocument
type ExpectedResult = {
    _id: string;
    updatedBy?: string;
};

// The actual structure of FooJsonDocument
type ActualResult = {
    _id: string;
    updatedBy?: ObjectId;
};

All mandatory properties of type ObjectId are successfully changed to string, however, none of the optional properties of type ObjectId are modified.

How can I adjust JsonDocument to correctly handle optional properties as well?

I found this question on Typescript change property type recursively, but it doesn't address optional properties, so I believe it's not an exact duplicate. Nonetheless, it does provide some valuable insights.

Answer №1

The issue lies in the fact that

(ObjectID | undefined) extends ObjectID
is false, as undefined extends ObjectID is also false. This leads to taking the negative path of the conditional statement.

If you are unconcerned about optional values, you can eliminate undefined from the type before conducting the test. By doing so, ObjectID extends ObjectID evaluates to true, enabling you to take the positive conditional route.

This can be accomplished either by mandating all properties or by removing nullability from each property during testing NonNullable<D[K]>.

type JsonDocument<D> = {
    [K in keyof D]:
        NonNullable<D[K]> extends ObjectId ? string
        : D[K] extends object ? JsonDocument<D[K]>
        : D[K];
};

View live example on typescript playground

Answer №2

This is my preferred approach for handling JSON data:

type Unserializable = Function | RegExp | Error;

export type ToJSON<T> = T extends undefined
    ? ToJSON<T extends undefined ? never : T> | undefined
    : T extends Unserializable
    ? never
    : T extends { toJSON: () => any }
    ? ReturnType<T['toJSON']>
    : T extends object
    ? {
            [K in keyof T]: ToJSON<T[K]>;
      }
    : T extends (infer A)[]
    ? ToJSON<A>[]
    : T;

When using the JSON.stringify function, it relies on the presence of a toJSON method to serialize the data effectively, making it suitable for various JSON-compatible types like ObjectId and Date.

This solution also accounts for optional properties, drawing inspiration from the concept of NonUndefined found in utility-types.

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 upload cannot be completed as the file upload is not in a multipart request format

This file upload code was referenced from this source. The issue lies in the fact that it is sending the request as JSON instead of multipart form data, causing the server side code to reject the request and resulting in a failed upload. I have confirmed ...

Angular: Updating image tag to display asynchronous data

Utilizing Angular to retrieve user profile pictures from the backend, specifically Node.js/Express, has been mostly successful. However, there is one issue that I have encountered. The HTML displaying the profile picture does not re-render when the user up ...

Step-by-step guide for importing a JSON file in React typescript using Template literal

I am facing an error while using a Template literal in React TypeScript to import a JSON file. export interface IData { BASE_PRICE: number; TIER: string; LIST_PRICE_MIN: number; LIST_PRICE_MAX: number; DISCOUNT_PART_NUM: Discout; } type Discoun ...

Imitate dependencies using Mocha and Typescript

In my typescript project, I am using mocha. Let's consider two modules: // http.ts export class Http { } // app.ts import Http from './http'; export class App { } I want to know how to mock the Http module while testing the App. The te ...

Encountered a Next-Auth Error: Unable to fetch data, TypeError: fetch failed within

I've been struggling with a unique issue that I haven't found a solution for in any other forum. My Configuration NextJS: v13.0.3 NextAuth: v4.16.4 npm: v8.19.2 node: v18.12.1 When the Issue Arises This particular error only occurs in the pr ...

Having trouble retrieving a specific key from the state object in Redux using TypeScript

I have incorporated TypeScript into my Ionic project, using React hooks. I recently added an errorReducer to handle any errors that may arise from the server. Below is a snippet of my code: Action import { ERROR_OCCURRED } from "./types"; ...

Importing node_modules in Angular2 using TypeScript

My Angular2 app started off as a simple 'hello world' project. However, I made the questionable decision to use different directory structures for my development environment and the final deployment folder on my spring backend. This difference c ...

What is the best approach for integrating a Material UI Autocomplete component with graphql queries?

Hello there! I'm currently working with React Typescript and trying to incorporate query suggestions into an Autocomplete Material UI component in my project. Below is a snippet of my GraphQL queries: Query Definition: import gql from 'graphql- ...

Angular is unable to access functions or variables within a nested function

I am currently utilizing google.maps.geocoder to make location requests within my Angular app. When I provide a callback function with the results, it leads to unexpected code breaks when trying to call another function that displays map markers. It seem ...

Angular's observables were unable to be subscribed to in either the constructor or ngOnInit() functions

Currently, I am incorporating an observable concept into my application. In this setup, a service is called by component1 to emit an event that is then subscribed to by component 2. Below is the code snippet for reference: Service Code export class Mes ...

"Implementing an abstract method in a class by overloading it with a generic type that

// Greetings from the TypeScript Playground, a platform where you can experiment with TypeScript code. type Constructor<T> = new (...args: any[]) => T; class ServiceChecklistResponse { } class AnotherModel { } abstract class AbstractView { ...

Exploring Angular component testing through jasmine/karma and utilizing the spyOn method

I have been facing an issue while trying to test my component. Even though the component itself works perfectly, the test keeps generating error messages that I am unable to resolve. Here is the snippet of code that I am attempting to test: export cl ...

A: TypeScript Error TS7006: Parameter implicitly has an 'any' type

TS7006: The parameter 'port' is implicitly assigned an 'any' type. constructor(port) { TS7006: The parameter 'message' is implicitly assigned an 'any' type. Emit(message) { I'm confused because all the other r ...

What sets enum with string values apart from a string type union in TypeScript?

When it comes to defining a variable with a predefined set of values in TypeScript code, there are two approaches - using an enum or using a union. For instance, imagine we have a button with various variants such as primary, secondary, and tertiary. We ...

calculate the difference between two dates and then add this difference to a new date

Utilizing TypeScript for date calculations. Example: Initial Date 1: "10/06/2021 10:10:05" Initial Date 2: "08/06/2021 11:10:05" Calculate the difference between the two dates, including date/month/year/hour/min/sec/milliseconds. Ensure compatibility wi ...

Typescript support on Emacs

"Is there a way to enable Typescript syntax highlighting in Emacs?" I have been struggling with this for quite some time. Using Emacs 24 on an Ubuntu Virtualbox VM, I can't seem to get package-refresh-contents to work as it just hangs on "Contacting ...

Altering or including new space variables within a custom Chakra-ui theme

Looking to customize spacing variables in a Chakra UI theme? I have successfully implemented various extensions, but changes to spacing are not being applied. const config: ThemeConfig = { initialColorMode: 'light', useSystemColorMode: false ...

The issue with functions not executing when triggered by HammerJS

In my application, there is a component that displays information for different days as they are cycled through using the functions dayUp() and dayDown(). Here is an example of how these functions are structured: dayUp() { if (this.dayCount == 7) { ...

Exploring the Augmented Theme in Material UI Using TypeScript and the "keyof" Operator

I've recently started working with TypeScript and I'm facing an issue that I can't seem to solve. Here's the problem: I extended my Material UI theme with the following types: declare module '@material-ui/core/styles/createPalette& ...

Encountering a typescript error: Attempting to access [key] in an unsafe manner on an object of

I have recently developed a thorough equality checking function. However, I am encountering an issue with the highlighted lines in my code. Does anyone have any suggestions on how to rectify this problem (or perhaps explain what the error signifies)? Her ...