Restricted inclusive collection utilizing embedded identifier

Attempting to segregate a discriminated union array into separate arrays of its union types has presented some challenges. I came across this particular question that provides generic discriminators.

Unfortunately, the dataset I am working with doesn't have the discriminator key at the topmost level.

type Foo = { meta: {type: "Foo"} };
type Goo = { meta: {type: "Goo"} };
type Union = Foo | Goo;

To address this issue, I made modifications by adjusting the discriminator function mentioned in the aforementioned post to anticipate the inclusion of the "meta" object one level beneath.

function discriminateNested<K extends PropertyKey, V extends string | number | boolean>(
    discriminantKey: K, discriminantValue: V
) {
    return <T extends Record<PropertyKey, any>>(
        obj: T & Record<"meta", Record<K, V extends T["meta"][K] ? T["meta"][K] : V>>
    ): obj is Extract<T, Record<"meta", Record<K, V>>> =>
        obj["meta"][discriminantKey] === discriminantValue;
}

Is there a more general approach to handle this? Perhaps by defining a nested key as a string like

discriminateNested("meta.type", "Foo")
?

Answer №1

To achieve discriminating based on the value at a nested property, one approach could be to have the function discriminateNested() accept a tuple of keys and then use that information for discrimination. While it's possible to do this with dot-separated strings, using tuples provides a more straightforward typing solution.

First, you would define NestedRecord<K, V>, where K is a tuple of keys. For example,

NestedRecord<["a", "b", "c"], string>
should represent {a: {b: {c: string}}}. This definition could look like:

type NestedRecord<K extends PropertyKey[], V> =
  K extends [infer K0 extends PropertyKey, ...infer KR extends PropertyKey[]] ?
  { [P in K0]: NestedRecord<KR, V> } : V;

This definition utilizes recursive conditional types along with variadic tuple types to construct the desired nested structure.

The implementation of discriminatedNested() could resemble this:

function discriminateNested<K extends PropertyKey[], V extends string | number | boolean>(
  discriminantKeys: [...K], discriminantValue: V
) {
  return <T extends NestedRecord<K, string | number | boolean>>(
    obj: T
  ): obj is Extract<T, NestedRecord<K, V>> =>
    discriminantKeys.reduce<any>((acc, k) => acc[k], obj) === discriminantValue;
}

This function takes discriminantKeys as a tuple type K (using [...K] notation) and a discriminantValue of type

V</code. It returns a custom type guard that extracts union members assignable to <code>Nestedrecord<K, V>
. The nested indexing operation is performed using reduce() on discriminantKeys.


Let's test the functionality:

type Foo = { meta: { type: "Foo" }, a: string };
type Goo = { meta: { type: "Goo" }, b: number };
type Union = Foo | Goo;

const discFoo = discriminateNested(["meta", "type"], "Foo");

const u: Union = Math.random() < 0.5 ?
  { meta: { type: "Foo" }, a: "abc" } : { meta: { type: "Goo" }, b: 123 };

if (discFoo(u)) {
  u // Foo
  console.log(u.a.toUpperCase());
} else {
  u // Goo
  console.log(u.b.toFixed(2));
}

The discFoo type guard function successfully narrows the type based on the discriminator key and value. When applied to the input data of type Union, it correctly filters out the appropriate union members allowing for precise type checking.

Playground link available here

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

Absence of "Go to Definition" option in the VSCode menu

I'm currently working on a Typescript/Javascript project in VSCODE. Previously, I could hover my mouse over a method to see its function definition and use `cmd + click` to go to the definition. However, for some unknown reason, the "Go to Definition" ...

Creating a String Array and linking it to an Input Field

I'm currently working on a code that involves mapping through an array of strings using observables. My objective is to display the value from this array inside an input field. However, despite being able to view the array in the console, I encountere ...

The specified property is not present in the type '{}'

I've been incorporating Typescript into my React application Within my mapStateToProps, this is the code I'm using const mapStateToProps = (state: AppState) => { console.log(state) return { ...state.player, position: ...

Leveraging Angular2's observable stream in combination with *ngFor

Below is the code snippet I am working with: objs = [] getObjs() { let counter = 0 this.myService.getObjs() .map((obj) => { counter = counter > 5 ? 0 : counter; obj.col = counter; counter++; return view ...

Is there a way to retrieve keys of the object from this combination type?

Can someone please help me understand how to retrieve keys from this union type? The Value is currently being assigned as a never type. I would like the Value to be either sno, key, or id type Key = { sno: number } | { key: number } | { id: number }; typ ...

The child component is receiving null input data from the Angular async pipe, despite the fact that the data is not null in the

I encountered a strange scenario that I'm unable to navigate through and understand how it occurred. So, I created a parent component called SiteComponent. Below is the TypeScript logic: ngOnInit(): void { this.subs.push( this.route.data.subscribe( ...

The formControl value is not being shown on the display

I am facing an issue with a form that has multiple fields created using formGroup and formControlName. The challenge arises when I launch the application and need to retrieve data from the back end to display it on the view. The specific problem I'm ...

The compiler option 'esnext.array' does not provide support for utilizing the Array.prototype.flat() method

I'm facing an issue with getting my Angular 2 app to compile while using experimental JavaScript array features like the flat() method. To enable these features, I added the esnext.array option in the tsconfig.json file, so the lib section now includ ...

The module has defined the component locally, however, it has not been made available for

I have developed a collaborative module where I declared and exported the necessary component for use in other modules. import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DateslideCompone ...

Create seamless communication between Angular application and React build

I am currently engaged in a project that involves integrating a React widget into an Angular application. The component I'm working on functions as a chatbot. Here is the App.tsx file (written in TypeScript) which serves as the entry point for the Rea ...

Unlock the power of Angular Component methods even when outside the Angular library with the help of @ViewChild()

In my Angular library, there is a component called AComponent which has its own template, methods, and properties. Once the Angular library is packaged, it becomes available as a NuGet package for other projects to utilize. @Component({ selector: ' ...

The navigation function in Angular, this.router.navigate, is causing issues and

I've encountered a peculiar issue. There's a logout function that is activated whenever I receive a 401 response from any API I interact with. The function looks like this: constructor( private router: Router, ) {} logout(router1: Router ...

Do const generics similar to Rust exist in TypeScript?

Within TypeScript, literals are considered types. By implementing const-generics, I would have the ability to utilize the value of the literal within the type it belongs to. For example: class PreciseCurrency<const EXCHANGE_RATE: number> { amount ...

Converting ts files to js: A comprehensive guide

I am looking for a way to transform my TypeScript files into JavaScript files smoothly. The conversion process goes well with the command: nodemon --watch assets/ts --exec tsc assets/ts/*.ts --outDir assets/js However, I have encountered an issue where im ...

Encountered an error while trying to access a property that is undefined - attempting to call

In my TypeScript class, I have a method that retrieves a list of organizations and their roles. The method looks like this: getOrgList(oo: fhir.Organization) { var olist: orgRoles[] = []; var filtered = oo.extension.filter(this.getRoleExt); f ...

Steps for converting TypeScript code to JavaScript using jQuery, without the need for extra libraries or frameworks like NPM

My single-page dashboard is quite basic, as it just displays weather updates and subway alerts. I usually refresh it on my local machine, and the structure looked like this: project/ index.html jquery-3.3.1.min.js script.js I decided to switch it t ...

Two distinct arrays interacting and syncing with each other through a single API request

I find myself in a strange situation. I am dealing with two objects: sessionBase: ISessionViewWrapperModel = <ISessionViewWrapperModel>{}; sessionDisplay: ISessionViewWrapperModel = <ISessionViewWrapperModel>{}; After making an api call, both ...

Extract all objects from an array where a specific field contains an array

data:[ { id:1, tags:['TagA','TagB','TagC'] }, { id:2, tags:['TagB','TagD'] }, { id:3, tags:[&a ...

How can I filter an array by a nested property using Angular?

I need help with objects that have the following format: id: 1, name: MyObj properties: { owners: [ { name:owner1, location: loc1 }, { name:owner2, location: loc1 } ] } Each object can have a different number of owners. I' ...

Tips for positioning a div alongside its parent div

I have a unique setup with two nested divs that are both draggable. When I move the parent div (moveablecontainer), the child div (box.opened) follows smoothly as programmed. Everything works well in this scenario. However, when I resize the browser windo ...