Leveraging Typescript's robust type system to develop highly specific filter functions

I'm attempting to utilize the robust TypeScript type system in order to construct a highly typed 'filter' function that works on a collection (not just a simple array). Below is an illustration of what I am striving for:

type ClassNames = 'B' | 'C' | 'D'; // The complete list is known

declare class A {
    protected constructor();
    className: ClassNames;
    attr1: string;
}

declare class B extends A {
    private constructor();
    className: 'B';
    attr2: string;
    attr4: number;
}

declare class C extends A {
    private constructor();
    className: 'C';
    attr3: number;
    attr4: string;
}

declare class D extends A {
    private constructor();
    className: 'D';
    attr4: string | number;
}

declare class Filter<T> {
    has<U extends keyof (A|B|C|D), V extends U[keyof U]>(propName: U, propVal: V): Filter<T & {U: V}>;
    //                                                                                         ^ This is clearly wrong
    all(): T[];
}

let g = new Filter<A>();
let x = g.has('className', 'B');
let y = g.has('attr4', 'whatever');
type Y = typeof y; // Desired output: Filter<C | D>
type X = typeof x; // Desired output: Filter<B>

A few points to consider:

  • I am acquainted with the entire class hierarchy and all classes are direct descendants of A.
  • The class declarations will be generated through a program, so verbosity or repetition is not a concern
  • I am open to using the latest beta version of TypeScript
  • Currently, my focus is solely on the declarations as the implementation appears relatively straightforward

I have experimented with conditional types without success (possibly due to limited knowledge in this area). There is also a thought that infer may play a role in the solution, though its application eludes me at present.

Is this goal attainable?

Answer №1

One approach to consider is refactoring to a structure like this. Initially, defining a type that encompasses the union of all valid subclasses of A can be useful; I've named this Classes. If necessary, you can then derive the type ClassNames:

type Classes = B | C | D;
type ClassNames = Classes["className"];

Next, utility types need to be established to facilitate describing the intended behavior of Filter<T>.


In light of a union type T, we aim for AllKeys<T> to provide the union of keys present in any of its components. A standard keyof T isn't sufficient since a value like

{a: string, c: string} | {b: number, c: string}
is recognized only to hold a key labeled as c; uncertainty exists regarding whether a is a key or not, thereby causing keyof to return "c". We desire
"a" | "b" | "c"
instead. Thus, AllKeys<T> must distribute keyof across unions within T. Here's the approach:

type AllKeys<T> =
  T extends unknown ? keyof T : never;

This represents a distributive conditional type.

A method similar to conducting indexed accesses on a union type T with an uncertain presence of every member possessing a certain key

K</code is necessitated. This can be termed as <code>SomeIdx<T, K>
. Again, straightforwardly using
T[K]</code wouldn't suffice as indexing into a key unknown to be existent within a type is prohibited by the compiler. Consequently, indexed accesses must also be distributed across unions in <code>T
:

type SomeIdx<T, K extends PropertyKey> =
  T extends unknown ? K extends keyof T ? T[K] :
  never : never;

Lastly, Select<T, K, V> should be written so as to pick out the Union(s) within type T known to contain a key

K</code and where type <code>V</code is considered suitable for the property at said key. This constitutes the filtering operation sought after. Yet again, the operation needs to be distributed across unions within <code>T</code; for each such member, it should be checked if <code>K</code stands as a known key and if <code>V</code aligns with the value type associated with that key:</p>
<pre><code>type Select<T, K extends PropertyKey, V> =
  T extends unknown ? K extends keyof T ? V extends T[K] ? T :
  never : never : never;

These are the utility types required, and now Filter<T> can be defined:

declare class Filter<T extends Classes = Classes> {
  has<K extends AllKeys<T>, V extends SomeIdx<T, K>>(
    propName: K, propVal: V): Filter<Select<T, K, V>>
  all(): T[];
}

Note the limitation imposed where T is expected to be compatible with Classes, signifying the union of recognizable subclasses of A. To prevent A itself from being included here, because it shouldn't appear within the resultant type of has(), T defaults to

Classes</code indicating that <code>Filter
on its own denotes Filter<B | C | D>.

For any given T, which reflects the existing set of subclasses of

A</code filtered down by <code>Filter<T></code, the <code>has()
function ought to accept a propName argument represented by a type
K</code limited to keys assignable to <code>AllKeys<T></code (i.e., <code>propName
must correspond to one of the recognized keys among the types held within T). Similarly, a propVal parameter of type
V</code restricted to items deemed appropriate based on <code>SomeIdx<T, K>
is meant to be supported. Ultimately, returning
Filter<Select<T, K, V>></code will zero in on members showcasing a verifiable key <code>K</code alongside a fitting value type for <code>V
.


With the definition complete, let's put it to the test:

let g = new Filter(); // Filter<Classes>

let x = g.has('className', 'B');
type X = typeof x; // type X = Filter<B>

let y = g.has('attr4', 'whatever');
type Y = typeof y; // type Y = Filter<C | D>

let z = x.has('attr3', 12345); // error!
// Argument of type '"attr3"' is not assignable to parameter of type 'keyof B'.

Analysis indicates satisfactory results. Commencing with a Filter<Classes>, narrowing down to

Filter<B></code via <code>has('className', 'B')
becomes possible. Alternatively, focusing in on
Filter<C | D></code through <code>has('attr4', 'whatever'
) proves viable since both B and
D</code would accept a <code>string
-valued attr4 attribute. When dealing with
Filter<B></code specifically, solely allowing a <code>propName
aligned with
B</code restricts scenarios like <code>"attr3"
from being accommodated.

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

Access the JSON data containing sub array values and showcase them on an HTML page by utilizing ngFor

Greetings! I am currently working on a web application where I need to showcase student data that is being received in JSON format. Below is the TypeScript code snippet that outlines the structure of the student data: export interface studentData{ ...

Ways to restrict users from inputting alphabets in TextField using material ui

I've set up a TextField where users can only input positive digits. Currently, I'm using the following onKeyDown event: <TextField label={distanceError} error={!!distanceError} defaultValue={kpoints.distance} on ...

Issue: Unable to locate 'child_process' in Angular 5

I am a newcomer to Angular, and I have encountered a requirement in my project to retrieve the MAC address of the user's system. To achieve this, I performed an NPM installation as shown below: npm install --save macaddress Next, I added the follow ...

Error in Angular: The use of decorators in this context is not allowed.ts(1206)

In my current project using Angular 17 and PrimeNG 17, I am implementing a theme switching feature. I have been following a tutorial from the Primeng documentation at this link: https://www.youtube.com/watch?v=5VOuUdDXRsE&embeds_referring_euri=https%3A ...

Calculate the date input in Angular 7 by subtracting calendar days

As a user of this Angular 7 application, you are required to choose a date from a calendar input and then determine what day it was 40 days ago. Unfortunately, I have not been able to accomplish this in Typescript yet. There are numerous JavaScript solutio ...

Issue encountered while trying to iterate through an observable: Object does not have the capability to utilize the 'forEach' property or method

I am currently following the pattern outlined in the hero.service.ts file, which can be found at this link: https://angular.io/docs/ts/latest/guide/server-communication.html The Observable documentation I referenced is available here: When examining my c ...

Why is my root page not dynamic in Next.js 13?

I am currently working on a website using Next.js version 13.0. After running the next build command, I noticed that all pages are functioning properly except for the root page. The issue is that it's being generated as a static page instead of dynami ...

What steps do I need to take in order to activate scrolling in a Modal using Material-UI

Can a Modal be designed to work like a Dialog with the scroll set to 'paper'? I have a large amount of text to show in the Modal, but it exceeds the browser window's size without any scrolling option. ...

Is there a way to disable tslint warnings for npm linked packages?

Currently, I am in the process of developing a set of Angular/TypeScript components within an application that utilizes this package. To facilitate the sharing of these components, I have utilized npm link. However, upon building the project, I encountered ...

Reduce the size of a container element without using jquery

In my Angular application, I have structured the header as follows: -- Header -- -- Sub header -- -- Search Box -- -- Create and Search Button -- -- Scroll Div -- HTML: <h1> Header </h1> <h3> Sub header </h3> <div class="s ...

What could be causing the presence of a "strike" in my typescript code?

While transitioning my code from JavaScript to TypeScript for the first time, I noticed that some code has been struck out. Can someone explain why this is happening and what it signifies? How should I address this issue? Here's a screenshot as an exa ...

"Setting Up a Service in Angular: A Step-by-Step Guide

I am facing an issue with a root service that contains composition within it, as shown below: import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MapService { private rmap: RMap; ini ...

Can we verify if strings can serve as valid property names for interfaces?

Let's consider an interface presented below: interface User { id: string; name: string; age: number; } We also have a method defined as follows: function getUserValues(properties:string[]):void { Ajax.fetch("user", properties).then( ...

Limit the frequency of function calls in Typescript

Update: After some research, I've learned that throttle has the capability to drop excess function invocations, making it unsuitable for my needs. I am still seeking an idiomatic solution to process every item in a queue at an appropriate pace without ...

Is it possible to identify unauthorized utilization of web APIs within TypeScript?

Recently, I encountered an issue while using the URLSearchParams.size in my code. To my surprise, it didn't work on Safari as expected. Checking MDN's browser compatibility table revealed that Safari version 16.6 does not support this feature, un ...

React TypeScript error: Cannot access property "x" on object of type 'A | B'

Just starting out with react typescript and I've encountered the following typescript error when creating components: interface APIResponseA { a:string[]; b:number; c: string | null; // <- } interface APIResponseB { a:string[] | null; b:number; d: ...

Exploring the functionality of surveyjs in conjunction with react and typescript

Does anyone have any code samples showcasing how to integrate Surveyjs with React and TypeScript? I attempted to import it into my project and utilized the code provided in this resource. https://stackblitz.com/edit/surveyjs-react-stackoverflow45544026 H ...

Angular2- Retrieving configuration information during application launch

Implementing a method to load configuration data from an ASP.NET web API using HTTP at startup via APP_INITIALIZER. This approach was influenced by a discussion on Stack Overflow about loading configuration in Angular2 here and here. Snippet from app.modu ...

Create an array filled with multiple arrays containing objects

To achieve the desired array of array of objects structure, I need to populate the data like this: let dataObj = [ [ { content: "test1"}, { content: "test2"}, { content: "test3"} ], [ ...

Angular's table data display feature is unfortunately lacking

Below is a simple HTML code snippet: <div class="dialogs"> <div id="wrapper" > <p>{{createTestingConstant()}}</p> <ng-container *ngFor="let one of contacts"> <p>{{one ...