What is the process through which Typescript determines the property types of union types?

I am implementing a unique set of types to enable angular form typing:

import { FormArray, FormControl, FormGroup } from '@angular/forms';

export type DeepFormArray<T, U extends boolean | undefined = undefined> = T extends Array<any>
  ? never
  : T extends string | number | boolean
  ? FormArray<FormControl<T>>
  : U extends true
  ? FormArray<FormControl<T>>
  : FormArray<DeepFormGroup<T>>;

export type DeepFormGroup<T> = T extends object
  ? FormGroup<{
      [P in keyof T]: T[P] extends Array<infer K>
        ? DeepFormArray<K>
        : T[P] extends object
        ? DeepFormGroup<T[P]>
        : FormControl<T[P] | null>;
    }>
  : never;

My goal is to apply these types to a type with a string literal property acting as a type identifier. Here's an example scenario:

interface A {
    name: 'A'
}

interface B {
    name: 'B'
}

/** expected output
 * {name: 'A' | 'B'}
 */
type C = A | B;

interface D {
  prop: C[];
}

These are combined as follows:


// The initial example was not precise for my requirements,
// however, the corrections proposed by @jcalz resolved the issue

/** expected output 
 * FormGroup<
 *   {name: FormControl<'A' | null>}
 * > | FormGroup<
 *   {name: FormControl<'B' | null>}
 * >
 */
type OldActualType = DeepFormGroup<C>;

type OldNeededType = FormGroup<{name: FormControl<'A' | 'B' | null>}>

// The accurate rendition of my problem

/** expected output
 * FormGroup<{
 *    prop: FormArray<FormGroup<{
 *        name: FormControl<"A" | null>;
 *    }>> | FormArray<FormGroup<{
 *        name: FormControl<"B" | null>;
 *    }>>;
 * }>
 */
type ActualType = DeepFormGroup<D>;
type NeededType = FormGroup<{
    prop: FormArray<FormGroup<{
        name: FormControl<"A" | "B" | null>;
    }>>;
}>

It appears that TypeScript does not create a new type for C, but interprets it as two variants. When C is used, all paths are explored. Is there a way to adjust DeepFormGroup or the definition of ActualType so that the inferred type matches NeededType? I am using TypeScript 4.8. Here is a link to a TypeScript playground

Answer №1

In many cases, it is expected that type operations will spread out over union types. For example, if you have a type operation F<T> and apply it to a union like F<A | B | C>, the desired outcome is often that this is equivalent to the union

F<A> | F<B> | F<C>
. This concept is referred to as "F<T> distributes over unions in T".

However, there are exceptions to this rule, specifically with DeepFormGroup<T> and DeepFormArray<T>. In these cases, we must adjust the definitions to explicitly prevent this behavior.


When dealing with a homomorphic mapped type for a generic T of the form {[P in keyof T]: ⋯}, TypeScript automatically distributes it over unions in T. To avoid this, one way is to modify it to no longer be homomorphic by wrapping keyof T in something that maintains its evaluation but blocks homomorphism. One approach is using

{[P in Extract<keyof T, unknown>: ⋯}
. The Extract utility type filters unions, and due to unknown being the universal supertype, Extract<X, unknown> is equal to X.


Similarly, when working with a conditional type for a generic T of the form T extends ⋯ ? ⋯ : ⋯, TypeScript distributes it over unions in

T</code, creating what is known as distributive conditional type. To prevent this, a non-distributive conditional type can be created by wrapping both sides of the check in arrays or tuples like <code>[T] extends [⋯] ? ⋯ : ⋯
.


The updated code would look like:

 // Code snippet here
  

This modification results in the desired types being achieved, providing a solution to the stated problem. If there are situations where distributivity is required for certain parts of these types, further refactoring may be necessary. However, that aspect is beyond the scope of this discussion.

Playground link to code

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

Utility managing various asynchronous tasks through observables and signaling mechanism

Looking for a Signal-based utility to monitor the status of multiple asynchronous operations carried out with observables (such as HTTP calls). This will enable using those signals in Components that utilize the OnPush change detection strategy. Imagine h ...

Asynchronous task within an if statement

After pressing a button, it triggers the check function, which then executes the isReady() function to perform operations and determine its truth value. During the evaluation process, the isReady() method may actually return false, yet display "Success" i ...

Creating a dynamic union type in Typescript based on the same interface properties

Is it possible to create a union type from siblings of the same interface? For example: interface Foo { values: string[]; defaultValue: string; } function fooo({values, defaultValue}: Foo) {} fooo({values: ['a', 'b'], defaultVal ...

What steps do I need to take to ensure that the external API proxy for Angular 8 functions properly, without automatically defaulting to

In my current project, I'm attempting to set up a development Angular application to streamline the process of writing and testing services for our main NativeScript app in production. One of the challenges I've encountered is dealing with the C ...

Modifying the menu with Angular 4 using the loggedInMethod

Struggling to find a solution to this issue, I've spent hours searching online without success. The challenge at hand involves updating the menu item in my navigation bar template to display either "login" or "logout" based on the user's current ...

How to Append Data to an Empty JSON Object in Angular

I have started with an empty JSON object export class Car {} After importing it into my component.ts, I am looking to dynamically add fields in a loop. For example: aux = new Car; for (var i = 0; i < 10; i++) { car.addName("name" + i); } My aim is ...

Exploring NextJS with Typescript

Struggling to incorporate Typescript with NextJS has been a challenge, especially when it comes to destructured parameters in getInitialProps and defining the type of page functions. Take for example my _app.tsx: import { ThemeProvider } from 'styled ...

Issue with Nestjs validate function in conjunction with JWT authentication

I am currently implementing jwt in nest by following this guide Everything seems to be working fine, except for the validate function in jwt.strategy.ts This is the code from my jwt.strategy.ts file: import { Injectable, UnauthorizedException } from &ap ...

The 'locale' parameter is inherently assigned the type of 'any' in this context

I have been using i18n to translate a Vue3 project with TypeScript, and I am stuck on getting the change locale button to work. Whenever I try, it returns an error regarding the question title. Does anyone have any insights on how to resolve this issue? ...

Nullable Object in Vue 3 Composition API

I am utilizing the Vue 3 Composition api along with Typescript to create pinch zoom functionality using the HammerJS package. In my Vue application, I am attempting to replicate a functional example implemented in JavaScript from CodePen: https://codepen. ...

What is the best way to update my list after deleting an item using promises?

I am facing an issue in my Angular application where I need to update a list after deleting an item, but I am unsure about how to achieve this using promises. delete(id: any) { this.missionService.deleteMission(id); // .then((res) => { // ...

What is the sequence in which Karma executes its tests?

When running my Karma tests through Jenkins, I have noticed that sometimes when a test fails, it only shows the test number without the test name. Is there a specific order in which Karma runs its tests? Perhaps alphabetically? Attached is a screenshot of ...

Removing a Request with specified parameters in MongoDB using NodeJS

Working with Angular 4 and MongoDB, I encountered an issue while attempting to send a delete request. My goal was to delete multiple items based on their IDs using the following setup: deleteData(id) { return this.http.delete(this.api, id) } In order ...

Access-Control-Allow-Methods is not permitting the use of the DELETE method in the preflight response for Angular 2

I am having trouble deleting a record in mongodb using mongoose. Here is the code snippet from my component: deleteProduct(product){ this._confirmationService.confirm({ message: 'Are you sure you want to delete the item?', ...

Encountering the error message `TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"` with `ts-node` when the type is specified as module

After configuring absolute paths in my Express project and changing the type to module for using import, I encountered an error: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" Below is the content of my tsconfig.json { &q ...

Generating a customizable PDF using jSPDF

I am in need of creating a dynamic PDF document with text content that could change along with their positions. The header columns are defined as an object array below: [ { "title": "Occupied Vacancies", "dataK ...

In TypeScript, there is a mismatch between the function return type

I am new to TypeScript and trying to follow its recommendations, but I am having trouble understanding this particular issue. https://i.stack.imgur.com/fYQmQ.png After reading the definition of type EffectCallback, which is a function returning void, I t ...

Concealing the sidebar in React with the help of Ant Design

I want to create a sidebar that can be hidden by clicking an icon in the navigation bar without using classes. Although I may be approaching this incorrectly, I prefer to keep it simple. The error message I encountered is: (property) collapsed: boolean ...

Is there a way to retrieve the date of the most recent occurrence of a specific "day" in TypeScript?

Looking to retrieve the date (in YYYY-MM-DD format) for the most recent "Wednesday", "Saturday", or any user-specified day. This date could be from either this week or last week, with the most recent occurrence of that particular day. For instance, if toda ...

The node version in VS Code is outdated compared to the node version installed on my computer

While working on a TypeScript project, I encountered an issue when running "tsc fileName.ts" which resulted in the error message "Accessors are only available when targeting ECMAScript 5 and higher." To resolve this, I found that using "tsc -t es5 fileName ...