Creating a TypeScript recursive type remapper to handle optional keys with unique mappings

I have a set of utility functions that validate the type of a variable. Some examples include string(), non_empty_string(), array(), non_null_object(), and others. These functions are all predicate functions that return a boolean value (although they do not follow the conventional naming pattern is<TypeName>()). All these utility functions belong to an object of type Utility.

interface Utility {
  string: (v: unknown) => v is string;
  number: ...;
  natural_number: ...;
  array: ...;
  non_empty_array: ...;
  ...
  ...
}

type UtilityTypes = keyof Utility;

Now, I want to create a validation function that can validate objects using these utility methods. For example, if I have a user object of type User,

interface User {
  name: string;
  age: number;
  isStudent?: boolean;
  address: {
    city: string;
    state: string;
    phone?: string;
  }
}

I would like to define a schema like the following:

type UserValidatorSchema = {
  readonly name: UtilityTypes;
  readonly age: UtilityTypes;
  readonly "isStudent?": UtilityTypes;
  readonly address: {
    readonly city: UtilityTypes;
    readonly state: UtilityTypes;
    readonly "phone?": UtilityTypes;
  }
}

const userSchema: UserValidatorSchema = {
  name: "non_empty_string",
  age: "natural_number",
  "isStudent?": "boolean";
  address: {
    city: "non_empty_string";
    state: "non_empty_string";
    "phone?": "non_empty_string";
  }
}

All optional properties in the schema should end with a "?" character to indicate that they are optional for the validator function.

My question now is whether there is a way to automatically generate the UserValidatorSchema based on the given User type?

Answer №1

I managed to find a solution on my own.

Check out the ValidatorSchema remapper below:

type ValidatorSchema<Type extends object> = {
  +readonly [key in keyof Type as ModifyOptionalKey<Type, key>]-?: NonNullable<
    Type[key]
  > extends object
    ? ValidatorSchema<NonNullable<Type[key]>>
    : UtilityTypes;
};

type ModifyOptionalKey<
  Type,
  Key extends keyof Type
> = undefined extends Type[Key] ? `${Key & string}?` : Key;

//---------------- Usages ---------------
interface User {
  name: string;
  age: number;
  isGoldUser?: boolean; 
}

type UserSchema = ValidatorSchema<User>;
/*
  UserSchema = {
    readonly name: UtilityTypes;
    readonly age: UtilityTypes;
    readonly "isGoldUser?": UtilityTypes
  } 
*/

Explanation


ModifyOptionalKey

This key remapper changes the names of optional fields to <fieldName>? by adding a "?" character at the end. It uses conditional type and template literal for this purpose.

interface User {
  name?: string;
}

In this example, the type of the name field is actually string | undefined because it is optional and may not be present in the object, defaulting to undefined. The statement

undefined extends Type[Key] ? `${Key & string}?` : Key;

determines whether a property is optional - if it's optional, it converts it to <property>? using `${Key & string}?`, otherwise it returns the original property.

ValidatorSchema

+readonly [key in keyof Type as ModifyOptionalKey<Type, key>]-?

  1. +readonly makes every property read-only
  2. [key in keyof Type as ModifyOptionalKey<Type, key>]
    remaps the optional keys
  3. -? removes the optional modifier from any property, making it required

After remapping the keys, attention turns to the property types. Each property in the validator schema can either be of type UtilityTypes or another ValidatorSchema. If a property is an object, then its type should also be a ValidatorSchema.

This is achieved through a conditional type:

Type[key] extends object ? ValidatorSchema<Type[key]> : UtilityTypes;

If Type[key] refers to an object, its ValidatorSchema is generated with a recursive call (ValidatorSchema<Type[key]>). If it's a primitive type, UtilityTypes is returned instead.

However, a problem arises when dealing with optional sub-schemas. For instance, consider the address field in the following User type:

interface User {
  name: string;
  address?: {
      city: string;
      state: string;
  }
}

The address field represents a sub-schema within the ValidatorSchema<User> schema, with a type of

{city: string; state: string} | undefined
.

type ReturnType = User["address"] extends object
  ? ValidatorSchema<User["address"]>
  : UtilityTypes

// ReturnType = UtilityTypes

Due to undefined not extending object, we obtain UtilityTypes. To remove undefined from User["address"]'s type, the utility type NonNullable is used.

The NonNullable<T> utility removes undefined and null from any union type. For example:

type nullableType = undefined | null | string;

type nonNullableType = NonNullable<nullableType>;
// nonNullableType = string

Therefore,

NonNullable<User["address"]>
results in {city: string; state: string}.

By applying NonNullable to the statement below, everything functions as intended:

NonNullable<Type[key]> extends object
    ? ValidatorSchema<NonNullable<Type[key]>>
    : UtilityTypes;

All of these concepts were learned from the "TypeScript Handbook".

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

Accessing the Parent Variable from a Function in JavaScript: A Guide

How can you properly retrieve the value of x? let x = 5 const f = (n:number) => { let x = "Welcome"; return x * n // Referring to the first x, not the second one } Also, what is the accurate technical term for this action? ...

Guide for inserting a new field into multipart/form-data before sending it to a different server?

Is there a way to upload a pdf to a NextJS endpoint, insert a userId field in the form-data, and then send it to another server for file storage? I have successfully forwarded the request as shown below, but I am struggling to figure out how to include an ...

Tips on utilizing index and eliminating React Warning: Ensure every child within a list has a distinct "key" prop

Hello, I am encountering an issue where I need to properly pass the index in this component. Can you help me figure out how to do that? Error: react-jsx-dev-runtime.development.js:117 Warning: Each child in a list should have a unique "key" prop ...

Exporting Modules in ES6 and beyond

Transitioning from the Java world, I am venturing into creating a vanilla JS (ES2018) Application with types documented in JSDOC. By using the TypeScript compiler, I aim to generate clean definition files that can be bundled with my app. With just two main ...

I am trying to replace the buttons with a dropdown menu for changing graphs, but unfortunately my function does not seem to work with the <select> element. It works perfectly fine with buttons though

I am currently working on my html and ts code, aiming to implement a dropdown feature for switching between different graphs via the chartType function. The issue I am facing is that an error keeps popping up stating that chartType is not recognized as a ...

Display issues with deeply nested components

I'm facing an issue with displaying the third nested component. Expected: Hello App Component Hello Nest-A Component Hello Nest-1 Component Hello Test-Z Component Actual: Hello App Component Hello Nest-A Component Hello Nest-1 Component Why ...

Encountering an error when using Webpack ModuleFederationPlugin and HMR: "Undefined properties cannot be set"

Description Hey everyone! I've been experimenting with Webpack ModuleFederationPlugin using React and TypeScript in my current proof of concept. Currently, I have two applications - ChildApp which exposes a module, and a HostApp that consumes this e ...

Issues with the functionality of Angular 2 routerLink

Unable to navigate from the homepage to the login page by clicking on <a routerLink="/login">Login</a>. Despite reviewing tutorials and developer guides on Angular 2 website. Current code snippet: index.html: <html> <head> ...

Bypass VueJs Typescript errors within the template section with Typescript

My VueJs App is functioning properly, but there is one thing that bothers me - a TypeScript error in my template block. Is there a way to handle this similar to how I would in my script block? <script setup lang="ts"> //@ignore-ts this li ...

Why are optional members utilized in a TypeScript interface?

Currently, I am engaged in an online tutorial on typescript (although it is not in English, I will translate the example): interface Message { email: string; receiver?: string; subject?: string; content: string; } The concept of the ...

typescriptIs it possible to disregard the static variable and ensure that it is correctly enforced

I have the following code snippet: export class X { static foo: { bar: number; }; } const bar = X.foo.bar Unfortunately, it appears that TypeScript doesn't properly detect if X.foo could potentially be undefined. Interestingly, TypeScript ...

Implementing dynamic Angular form group array with conditional visibility toggling

Within my angular application, I am faced with the task of implementing a complex form. Users should be able to dynamically add, remove, and modify elements within the form. Each element consists of a group of inputs, where some are conditionally hidden o ...

What is the method for verifying the types of parameters in a function?

I possess two distinct interfaces interface Vehicle { // data about vehicle } interface Package { // data about package } A component within its props can receive either of them (and potentially more in the future), thus, I formulated a props interface l ...

Refresh the project once logged in using angular

Thank you in advance for your response. I am facing an issue with monitoring user activity and automatically logging them out after a certain period of inactivity. I have successfully implemented this feature in my app.component.ts file, however, it only s ...

Unable to determine the size of the array being passed as a parameter in the function

When passing an array as a parameter to a function, I aim to retrieve its length. Within my code, I am working with an array of objects. groupList:group[]=[]; Within the selectItem function, I invoke the testExistinginArray function. selectItem (event) ...

Configuring Jest in an Angular 14 and Ionic 6 application

My current challenge involves setting up the Jest test framework in my project, which utilizes Angular 14 and Ionic 6. I am also using other potentially conflicting plugins such as Firebase and Ngrx. I have been primarily following this informative tutori ...

Create a new instance of the TypeScript singleton for each unit test

I have a TypeScript singleton class structured like this: export default class MySingleton { private constructor({ prop1, prop2, ... }: MySingletonConfig) { this.prop1 = prop1 ?? 'defaultProp1'; this.prop2 = prop2; ...

Issues with TypeScript Optional Parameters functionality

I am struggling with a situation involving the SampleData class and its default property prop2. class SampleData { prop1: string; prop2: {} = {}; } export default SampleData; Every time I attempt to create a new instance of SampleData without sp ...

The 'log' property cannot be found on the type '{ new (): Console; prototype: Console; }' - error code TS2339

class welcome { constructor(public msg: string){} } var greeting = new welcome('hello Vishal'); Console.log(greeting.msg); I encountered an error at the Console.log statement. The details of the error can be seen in the image attached below. ...

Generic type input being accepted

I am trying to work with a basic generic class: export class MyType<T>{} In the directive class, I want to create an @Input field that should be of type MyType: @Input field MyType<>; However, my code editor is showing an error that MyType& ...