How to selectively make properties optional in Typescript conditions

Currently, I am working on creating a utility type to unwrap nested monads of Options in my code. Here is the progress I have made so far:

export interface Option<T> {
  type: symbol;
  isSome(): boolean;
  isNone(): boolean;
  match<U>(fn: Match<T, U>): U;
  map<U>(fn: (val: T) => U): Option<U>;
  andThen<U>(fn: (val: T) => Option<U>): Option<U>;
  or<U>(optb: Option<U>): Option<T | U>;
  and<U>(optb: Option<U>): Option<U>;
  unwrapOr(def: T): T;
  unwrap(): T | never;
}

export type UnwrappedOptionsType<T> = T extends (infer U)[]
  ? UnwrappedOptionsType<U>[]
  : T extends object
  ? {
      [P in keyof T]: T[P] extends Option<infer R>
        ? UnwrappedOptionsType<R> | undefined
        : UnwrappedOptionsType<T[P]>;
    }
  : T;

My expectation is that the types will be inferred correctly and properties that are Options become optional. For example, consider the following type:

type SignUpRequest = {
    username: string;
    password: string;
    email: Option<string>;
}

When using

UnwrappedOptionsType<SignUpRequest>
, expected output should be:

{
    username: string;
    password: string;
    email?: string | undefined;
}

However, the actual result obtained is:

{
    username: string;
    password: string;
    email: string;
}

The type of the option is successfully inferred, but it does not accept undefined. How can I make these options optional?

Edit: Updated code for reproducibility. Also, the requirement is for properties to be explicitly optional, not just possibly undefined.

Answer №1

Consider the following implementation:

type UnwrapOptions<T> =
    T extends Option<infer U> ? UnwrapOptions<U> | undefined :
    T extends readonly any[] ? {[I in keyof T]: UnwrapOptions<T[I]>} :
    T extends object ? (
        { [K in keyof T as Option<any> extends T[K] ? never : K]: UnwrapOptions<T[K]> } &
        { [K in keyof T as Option<any> extends T[K] ? K : never]?: UnwrapOptions<T[K]> }
    ) extends infer U ? { [K in keyof U]: U[K] } : never :
    T;

This complex structure implements a recursive conditional type logic. It may exhibit unexpected behaviors or limitations due to specific edge cases, so thorough testing is recommended before practical application.

Examining this code further:

  • If UnwrapOptions<T> is evaluated with T set as...

    • ...an Option<U>, the function recursively computes UnwrapOptions<U> (in case of nested Options) and combines it with undefined in a union format.

    • ...an array or tuple type, each element goes through an iterative process applying UnwrapOptions.

    • ...a primitive type, it directly returns that primitive value.

    • ...a non-array object type, properties are separated into assignable and non-assignable types based on Option presence. These are then merged back together through intersections.


To test this functionality, consider the following examples:

type SignUpRequest = {
    username: string;
    password: string;
    email: Option<string>;
}

type UnwrappedSignupRequest = UnwrapOptions<SignUpRequest>;
/* Result:{
    username: string;
    password: string;
    email?: string | undefined;
} */

The expected output for the above scenario matches the intended behavior. Let's expand with a more intricate data structure:

interface Foo {
    a: string;
    b?: number;
    c: string[];
    d: { z?: string };
    e: Option<number>;
    f: Option<string>[];
    g: Option<string> | number;
    h: [1, Option<2>, 3];
    i: { y: Option<string> };
    j: Option<{ x: Option<{ w: string }> }>;
    k: Foo;
}

This results in:

type UnwrappedFoo = UnwrapOptions<Foo>;
/* Output:
    - Properties unwrapped individually
    - Recursive property (k) displayed as 'any' in IntelliSense but maintains its original sub-properties.
*/

Upon inspection, all properties within the complex Foo structure are appropriately handled by the function logic.


Access the TypeScript Playground for hands-on experimentation

Answer №2

In order to make the option optional in your UnwrappedOptionsType type, you'll need to incorporate an additional type guard:

type UnwrappedOptionsType<T> = T extends (infer U)[]
  ? UnwrappedOptionsType<U>[]
  : T extends object
  ? {
      [P in keyof T]: T[P] extends Option<infer R>
        ? (UnwrappedOptionsType<R> | undefined) | undefined
        : UnwrappedOptionsType<T[P]>;
    }
  : T;

By implementing the type guard, the UnwrappedOptionsType for the SignUpRequest type will be as follows:

type UnwrappedOptionsType<SignUpRequest> = {
    username: string;
    password: string;
    email?: (string | undefined) | undefined;
}

This will result in the following output:

{
    username: string;
    password: string;
    email?: string | undefined;
}

Does this align with what you are aiming for?

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

Using Flickity API in Vue 3 with Typescript Integration

I have encountered an issue with implementing Flickity in my Vue 3 application. Everything works perfectly fine when using a static HTML carousel with fixed cells. However, I am facing difficulties when attempting to dynamically add cells during runtime us ...

The Type X is lacking essential properties found in Type Y, including length, pop, push, concat, and an additional 26 more properties. Code: [2740]

One interesting thing I have noticed is the Product interface: export interface Product{ code: string; description: string; type: string; } There is a service with a method that calls the product endpoint: public getProducts(): Observable<Product ...

How to only disable checkboxes that are currently checked in Angular 8

click here to view an image**I would like to know how I can disable only the selected/checked items on a p-listbox in Angular 8. Is it possible to achieve this by adding a specific property or using CSS? Currently, when I try to add the [disabled] proper ...

Error: The property 'target' cannot be read

I'm seeking to enhance a value by pinpointing a specific element within a loop. <div *ngFor="let item of items; let i = index"> <ion-button (click)="increment(i)"> <ion-icon name="add"></ion ...

Creative Solution for Implementing a Type Parameter in a Generic

Within my codebase, there exists a crucial interface named DatabaseEngine. This interface utilizes a single type parameter known as ResultType. This particular type parameter serves as the interface for the query result dictated by the specific database dr ...

A guide on transforming a 1-dimensional array into a 2-dimensional matrix layout using Angular

My query revolves around utilizing Template (HTML) within Angular. I am looking for a way to dynamically display an array of objects without permanently converting it. The array consists of objects. kpi: { value: string; header: string; footer: string }[] ...

The HTML table is displaying with an offset, which is probably caused by the *ngFor directive

I'm having trouble aligning the HTML table properly, as it seems to be misaligned. The issue I am facing is related to the inner loop (modification) which is a list inside of Revision (basically, Revision 'has a' modification list). Althoug ...

Designations for removing an item at a targeted subdirectory location

type Taillet<T extends any[]> = ((...t: T) => void) extends (( h: any, ...r: infer R ) => void) ? R : never; type NestedOmit<T, Path extends string[]> = T extends object ? { 0: Omit<T, Path[0]>; 1: { [ ...

I am puzzled by this error in Typescript: "Why does the element have an 'any' type when the Object type lacks an index signature?"

Looking to extract an array of keys from an object with nested properties, my current code: public static getKeys(obj: Object) { let keys: string[] = []; for (let k in obj) { if (typeof obj[k] == "Object" && obj[k] !== null) { ...

What is the best way to enable external access to a class component method in React and Typescript?

I am currently working on a component library that compiles to umd and is accessible via the window object. However, I need to find a way to call a class component's methods from outside the class. The methods in my classes are private right now, but ...

Can an Angular Component be displayed using a Serverless function like Lambda on AWS?

I have a single-page application developed in JavaScript using the Angular 6 Framework, and I am interested in dynamically rendering an Angular Component that is hosted on a remote server. Currently, I am utilizing viewContainerRef to dynamically render ...

Issue connecting database with error when combining TypeORM with Next.js

I am attempting to use TypeORM with the next.js framework. Here is my connection setup: const create = () => { // @ts-ignore return createConnection({ ...config }); }; export const getDatabaseConnection = async () => { conso ...

Vuejs fails to properly transmit data

When I change the image in an image field, the new image data appears correctly before sending it to the back-end. However, after sending the data, the values are empty! Code Commented save_changes() { /* eslint-disable */ if (!this.validateForm) ...

Ways to eliminate the white background gap between pages on ionic

While developing an app using Ionic, I encountered a strange issue. Everything runs smoothly on a browser, but when testing the app on an Android 5 device, I noticed a white background appearing between pages. The app loads correctly with the custom splas ...

Exporting Typescript to Javascript files

I have created a sample TypeScript object with the following code: declare const S3 = "https://s3.amazonaws.com/xxx/icons"; declare const SVG = "svg-file-icons"; declare interface MyIcons { "image/jpeg": string; "image/jpg": string; } export const F ...

Unable to export Interface in Typescript - the specific module does not offer an export named Settings

I am encountering an issue while trying to export/import an interface in Typescript. The error message I receive is causing confusion as I'm unsure of where I went wrong. Uncaught SyntaxError: The requested module '/src/types/settings.ts' ...

Misunderstanding the concept of always being right

Here is a code snippet that raises an error in TypeScript: class Status { constructor(public content: string){} } class Visitor { private status: Status | undefined = undefined; visit(tree: Tree) { if (tree.value > 7) { this.status = new ...

Troubleshooting Next.js Route Redirect Failure to Origin URL

I'm currently facing a challenge in my Next.js project where I have a layout component nested inside the app directory. Within this layout component, there's a client-side navbar component that includes a logout button. The goal is to redirect th ...

Utilizing the output of one function as an input parameter for another function: A guide

Having this specific shape const shape = { foo: () => 'hi', bar: (arg) => typeof arg === 'string' // argument is expected to be a string because foo returns a string } Is there a way to connect the return type of foo to the ...

Show JSON information in an angular-data-table

I am trying to showcase the following JSON dataset within an angular-data-table {"_links":{"self":[{"href":"http://uni/api/v1/cycle1"},{"href":"http://uni/api/v1/cycle2"},{"href":"http://uni/api/v1/cycle3"}]}} This is what I have written so far in my cod ...