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