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

Having trouble with the "Vs Code nx console generate" command? It seems that there are no flags available to configure

My issue involves the nx console extension installed in my Visual Studio Code. Every time I attempt to use the generate command for components, services, or libraries, I receive an error message stating "ng generate @schematics/angular:component This com ...

Proper method for typing the generics of DatePickerProps belonging to the DatePicker component in mui-x library

I have a component called CustomDatePicker which has been configured for localization as shown below: function CustomDatePicker(props: DatePickerProps<unknown> & React.RefAttributes<HTMLDivElement>) { return ( <StyledDatePicker ...

Am I on track with this observation?

I am currently using the following service: getPosition(): Observable<Object> { return Observable.create(observer => { navigator.geolocation.watchPosition((pos: Position) => { observer.next(pos); observer.c ...

Can the arrow function properly subscribe to an Observable in Angular and what is the accurate way to interpret it?

I'm currently working through the official Angular tutorial: https://angular.io/tutorial/toh-pt4 Within this tutorial, there is a component class that subscribes to a service: import { Component, OnInit } from '@angular/core'; import { He ...

Database records failing to update after deployment

After deploying my next js site using Vercel, I encountered an issue with the functionality related to adding, getting, editing, and deleting data from MongoDB. Although all functions were working perfectly locally, once deployed, I noticed that while I co ...

How can TypeScript be used to enable CSV or PDF export in a material-react-table?

Is it possible to incorporate the ability to export data to CSV or PDF in a material-react-table? While I am familiar with how to do this with a Material UI table, I have not been able to find the same functionality for the material-react-table. Thank you ...

When the user clicks on a specific element, ensure that it is the main focus and generate an overlay

One of my challenges is implementing a custom element that captures user input upon clicking, focusing on it and overlaying other elements. I want the overlay to disappear if the user clicks outside the div. I attempted to achieve this using the iron-over ...

Do type declaration files for NPM packages have to be in the .d.ts format?

I believe it is feasible to include type declarations in any typescript file like '.d.ts', '.ts', or '.tsx'. However, I have observed that the type declaration files for most npm packages are .d.ts files. Is this a requireme ...

Encountering a problem with the 'string' parameter when using TypeScript

I keep encountering the following error message: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ barkingRoadProject: string[]; }'. No index signature with a paramet ...

Ensuring a precise data type in a class or object using TypeScript

I am familiar with Record and Pick, but I struggle to grasp their proper usage. My goal is to ensure that a class or object contains specific data types such as strings, Booleans, arrays, etc., while also requiring properties or fields of Function type. in ...

Conditional type/interface attribute typing

Below are the interfaces I am working with: interface Movie { id: number; title: string; } interface Show { title: string; ids: { trakt: number; imdb: string; tmdb?: number; }; } interface Props { data: Movie | Show; inCountdown ...

Display Module within Component using Angular 5

In the application I'm working on, I want to incorporate a variety of progress-loader-animations such as spinners or bars. To achieve this, I've developed a module with a component. Now, I'm trying to figure out how to display the module&ap ...

Struggling to implement the Pick utility type alongside the React useState hook

Can anyone explain why I am unable to utilize the Pick utility type in order to select a property from my interface and apply it to type my component's state? This is what my interface looks like: export interface IBooking { ... propertyId: strin ...

What steps are needed to generate an RSS feed from an Angular application?

I have a website built with Angular (version 12) using the Angular CLI, and I am looking to generate an RSS feed. Instead of serving HTML content, I want the application to output RSS XML for a specific route like /rss. While I plan on utilizing the rss p ...

Converting JSON Arrays into Typescript Arrays

I am working with a JSON file that contains an array object like this: [ { "VergiNo": "XXXXXXX" }, { "VergiNo": "YYYYYY" }, { "VergiNo": "ZZZZZZ" } ] After importing this JSON file into my Typescript file, import * as companies f ...

NGRX 8 reducer now outputting an Object rather than an Array

I am facing an issue where the data returned from the reducer is an object instead of an array. Despite trying to return action.recentSearches, it doesn't seem to work as expected. The data being returned looks like this: { "loading": false, "recent ...

Serious issue: a dependency request is an expression (Warning from Angular CLI)

I am currently exploring the dynamic loading of lazy child routes within a lazy routing module. For example: const serverResponse = [ { path: "transaction", children: [ { path: "finance", modulePath: &qu ...

What is the most efficient way to retrieve a single type from a union that consists of either a single type or an array of types

Is there a way to determine the type of an exported union type by extracting it from an array, as illustrated in the example above? How can this be achieved without directly referencing the non-exported Type itself? interface CurrentType { a: string; b ...

Exploring the world of chained JavaScript Promises for automatic pagination of an API

Dealing with a paged API that requires fetching each page of results automatically has led me to construct a recursive promise chain. Surprisingly, this approach actually gives me the desired output. As I've tried to wrap my head around it, I've ...

Whenever comparing the types 'string[]' and 'DeliveryTypeEnum', this condition will consistently result in 'true' as there is no intersection between the two. This is highlighted by the error code ts(2367)

Hello everyone, I'm a junior developer and could use some assistance if (query.deliveryType && query.deliveryType != DeliveryTypeEnum.EITHER) { search.push({ terms: { "deliveryType.keyword&q ...