One possible recommendation is to update
export type MockedInterface<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer B
? CalledWithMock<B, A> & T[K]
: MockedInterface<T[K]> & T[K];
};
to
export type MockedInterface<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer B
? T[K] & CalledWithMock<B, A>
: T[K] & MockedInterface<T[K]>;
};
This adjustment entails moving the intersection with T[K]
to the first part of each expression. In general, unless X
and Y
have matching function-typed signatures or properties, X & Y
is equivalent to Y & X
(specifically for intersections of function types, the order of overload call signatures matters when resolving overloads).
You can verify that this modification functions as intended:
foo(dm); // okay
The reason behind why this version performs better isn't definitively established. While it's true that order can impact performance at times (such as in microsoft/TypeScript#43437), there isn't explicit documentation regarding intersections like this.
A plausible explanation would be that the compiler evaluates an intersection type like F<T> & G<T>
by first computing F<T>
, then calculating G<T>
, and ultimately intersecting them. If G<T>
is straightforward to compute while F<T>
is complex, then
U extends G<T> & F<T>
might offer better performance compared to
U extends F<T> & G<T>
.
In your scenario, the compiler needed to assess if
MockedInterface<HTMLDivElement> extends Element
. To achieve this, it had to iterate through every property of
HTMLDivElement
and evaluate either
CalledWithMock<B, A> & T[K]
or
MockedInterface<T[K]> & T[K]
. Assuming it always assesses the left term initially, this could lead to recursive evaluations within each property and subproperty of
HTMLDivElement
before confirming assignability. If
HTMLDivElement
contains recursive definitions or numerous dependencies (both likely scenarios), the compiler may reach recursion limits.
Conversely, utilizing
T[K] & CalledWithMock<B, A>
or
T[K] & MockedInterface<T[K]>
allows the compiler to promptly recognize that each property
K
of
MockedInterface<HTMLDivElement>
is assignable to
HTMLDivElement[K]
, establishing that
MockedInterface<HTMLDivElement>
itself is assignable to
HTMLDivElement
without delving further.
This premise may or may not accurately depict reality; for an authoritative answer, consider raising an issue on TypeScript's GitHub repository to inquire about it (after conducting a thorough search for existing issues).
Irrespective of which branch of the conditional type is selected, since you're intersecting with
T[K]</code, it's plausible to shift it entirely out of the conditional type:</p>
<pre><code>type MockedInterface<T> = T & {
[K in keyof T]: T[K] & (T[K] extends (...args: infer A) => infer B
? CalledWithMock<B, A>
: MockedInterface<T[K]>);
}
Moving the intersection outside the mapped type altogether won't alter outcomes while simplifying evaluation even more (e.g., something akin to
{[K in keyof T]: T[K] & F<T, K>}
is essentially equivalent to
{[K in keyof T]: T[K]} & {[K in keyof T]: F<T, K>}
, which is essentially comparable to
T & {[K in keyof T]: F<T, K>}
. So, incorporating modifications such as:
type MockedInterface<T> = T & {
[K in keyof T]: T[K] extends (...args: infer A) => infer B
? CalledWithMock<B, A>
: MockedInterface<T[K]>;
}
Each enhancement incrementally facilitates the compiler in evaluating
MockedInterface<T></code. Certainly, the prompt confirmation of <code>MockedInterface<HTMLDivElement>
being compatible with
HTMLDivElement
helps streamline the assessment process, sparing the need for recursive exploration down the mapped type.
Hence, my final inclination leans towards this version, where
MockedInterface<T> = T & ...
. Naturally, testing against your distinct scenarios is essential; analogous to cases (e.g., overloads) where
X & Y
may vary from
Y & X
, some contexts might warrant disparities between this transposition using
T &
versus the previous nested subproperty intersection iteration (for instance, if
T
acts as a function type on its own,
{[K in keyof T]: T[K]}
wouldn't be callable despite
T
being so). Tailoring your implementation to align with your specific requirements becomes imperative if such distinctions hold significance.
Access Playground code here