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

Mistakes following update to Angular 4 from Angular 2

After upgrading from Angular2 to Angular4, I encountered these errors in the CLI. While my app continues to function after the upgrade, I am curious about possible solutions to resolve these errors. Any suggestions? https://i.stack.imgur.com/CyYqw.png He ...

Utilizing GroupBy in RxJs for an Observable of Objects数组

I am working with entries of type Observable<Event[]>, and they are structured as follows: [{ "_id": 1, "_title": "Test Event 1", "_startDate": "2019-05-29T07:20:00.000Z", "_endDate": "2019-05-29T08:00:00.000Z", "_isAllDay": false }, ...

Issues with Observable<boolean> functionality

Can anyone lend a hand? I'm facing a challenge with this function that is crucial for the application. Typescript File get $approved(): Observable<boolean> { return this.$entries.map(entries => { if (entries.length > 0) { ret ...

Mongodb Dynamic Variable Matching

I am facing an issue with passing a dynamic BSON variable to match in MongoDB. Here is my attempted solutions: var query = "\"info.name\": \"ABC\""; and var query = { info: { name: "ABC" } } However, neither of thes ...

What could be causing the errors I'm encountering in my TypeScript component within AngularJS?

I am working with an AngularJS component written in TypeScript called news.component.ts. This component makes a call to a service named google.service.ts in order to fetch news RSS using a service that converts XML to JSON. Within the NewsComponent, I hav ...

mat-table dataSource is not functioning properly with REST API integration

I'm facing an issue while trying to populate a Material Table with data. My model Block has fields such as id, date, etc. The API call is made in data.service and the function getAllBlock() fetches the blocks. I tested this in the app.component.html ...

I am attempting to code a program but it keeps displaying errors

What is hierarchical inheritance in AngularJS? I have been attempting to implement it, but I keep encountering errors. import {SecondcomponentComponent} from './secondcomponent/secondcomponent.Component'; import {thirdcomponentcomponent} from & ...

Creating an interface that extends the Map object in TypeScript to maintain the order of keys

After learning that the normal object doesn't preserve key order in TypeScript, I was advised to use Map. Nevertheless, I'm struggling to figure out how to assign values once I've declared the interface. Take a look at my approach: Coding ...

Ways to effectively test public functions in Typescript when using react-testing-library

I have come across the following issue in my project setup. Whenever I extend the httpService and use 'this.instance' in any service, an error occurs. On the other hand, if I use axios.get directly without any interceptors in my service files, i ...

Issues arise when upgrading from Angular 8 to 9, which can be attributed to IVY

After successfully upgrading my Angular 8 application to Angular 9, I encountered an error upon running the application. { "extends": "./tsconfig.json", "compilerOptions": { "outDir": ". ...

How can Jest be configured to test for the "permission denied" error?

In my Jest test, I am testing the behavior when trying to start a node http server with an invalid path for the socket file: describe('On any platform', (): void => { it('throws an error when trying to start with an invalid socket P ...

The test() function in JavaScript alters the output value

I created a simple form validation, and I encountered an issue where the test() method returns true when called initially and false upon subsequent calls without changing the input value. This pattern repeats with alternating true and false results. The H ...

One issue that may arise is when attempting to use ngOnDestroy in Angular components while rearranging user transitions

Encountered an issue recently with Angular - when the user navigates from component A to component B, component A remains active unless ngOnDestroy is triggered. However, if the user visits component B before going to component A and then leaves, ngOnDes ...

PhpStorm alerts users to potential issues with Object methods within Vue components when TypeScript is being utilized

When building Vue components with TypeScript (using the lang="ts" attribute in the script tag), there is a warning in PhpStorm (version 2021.2.2) that flags any methods from the native JavaScript Object as "Unresolved function or method". For exa ...

Angular 2 Routing 3.0: Paying Attention to Letter Case

let routesList: Routes = [ { path: 'x', component: xComponent }, { path: 'y', component: yComponent }, { path: 'zComponent', component: zComponent } ]; When entering "x" in the URL, it navigates to the component page. Ho ...

Is it possible to implement drag and drop functionality for uploading .ply, .stl, and .obj files in an angular application?

One problem I'm facing is uploading 3D models in angular, specifically files with the extensions .ply, .stl, and .obj. The ng2-upload plugin I'm currently using for drag'n'drop doesn't support these file types. When I upload a file ...

The functionality of the Ionic menu button becomes disabled once the user has successfully logged in

Having trouble clicking the button after taking a test. Situation: Once logged in -> user takes a test and submits -> redirected to home page. However, unable to click on "Menu button" on the home page. In my Login.ts file: if (this.checker == " ...

The push() method replaces the last item in an array with another item

Two objects are available: ej= { name="", code: "", namebusinessG:"", codebusinessG:"" }; group = { name:"", code:"" } Both of these objects will be stored in two arrays: groupList:any[]=[]; ejList:any[]=[]; The program flow s ...

What is the method for assigning a string to module variable definitions?

As someone new to TypeScript and MVC, I find myself unsure if I am even asking the right questions. I have multiple TypeScript files with identical functionality that are used across various search screens. My goal is to consolidate these into a single fil ...

Can you provide guidance on effectively utilizing a Pinia store with Vue3, Pinia, and Typescript?

I'm currently facing challenges while using the Pinia store with TypeScript and implementing the store within a basic app.vue Vuejs3 option api. Here is my app.js file: import {createApp} from 'vue' import {createPinia} from "pinia&quo ...