question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Excessive instantiation depth with fluent chaining API

See original GitHub issue

@amcasey reduced a perf complaint from https://github.com/glideapps/glide-code-columns/blob/master/src/columns/as-array.ts to this playground:

class Col<TParams> {
    public withRequiredPrimitiveParam<TName extends string>(name: TName) {
        return undefined! as Col<
            TParams & { readonly [K in TName]: any }
        >;
    }
}

new Col()
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value")
    .withRequiredPrimitiveParam("value") // Hits stack limit

Each additional chain results in a new instantiation of Col with an additional { value: any } intersected with the previous instantiation’s type argument. These intersections don’t seem to get simplified for some reason—hovering the third call in the chain yields the quick info

(method) Col<{ readonly value: any; } & { readonly value: any; }>.withRequiredPrimitiveParam<"value">(name: "value"): Col<{
    readonly value: any;
} & {
    readonly value: any;
} & {
    readonly value: any;
}>

Working with a more realistic simplification of the source, @amcasey measured type instantiations as a function of calls in the chain:

Chart showing exponential increase of instantiations, skyrocketing at around 12 calls

It seems that the function is the same even for this dead simple reproduction where each call after the first doesn’t meaningfully change the return type since we max out at the same number of calls, and it’s not obvious to me why that pattern is exponential.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:4
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

4reactions
ahejlsbergcommented, Nov 13, 2021

First, the easy workaround:

class Col<TParams> {
    public withRequiredPrimitiveParam<TName extends string>(name: TName) {
        return undefined! as Col<
            TParams & Record<TName, any>
        >;
    }
}

Basically, replace { readonly [K in TName]: any } with Record<TName, any>. What’s the difference? Well, in order to maximally reuse instantiations of anonymous object types we determine which in-scope type parameters are possibly referenced within the object type and then create a map keyed by the type IDs for which the type parameters are instantiated. The object type only references TName, so that should be the only type parameter contributing to the key. However, it is hard for us to prove that TParams isn’t referenced because there is intervening non-type-declaration code between the object type and the declaration of TParams. For example, there could be a local type that references TParams and a reference to that local type in the anonymous object type. For that reason, we view TParams as possibly referenced and include it in the instantiation key map. That in turn causes all the instantiations to look different to us, so we keep manufacturing new ones, making more and more types until we eventually hit our governor.

If we instead write Record<TName, any>, we effectively remove TParams from scope and all the instantiations collapse to a single cached type.

I’ll think about ways we can be smarter about concluding that type parameters aren’t referenced, but it’s not trivial.

1reaction
ahejlsbergcommented, Nov 13, 2021

Doesn’t Record<TName, any> expand to { [K in TName]: any }, though?

Yes, they are structurally identical. But they are still represented as two distinct type instances in the type checker (specifically, two distinct instances of the ObjectType type), and intersections only deduplicate types with the same object identity. When we use Record<K, T>, the instantiation cache is keyed by the type identities used for K and T. In the example there, that means all instantiations for the same TName are shared. But when we use { [K in TName]: any }, the instantiation cache is keyed by the type identities used for TParams and TName. And the fact that we can’t exclude TParams is what causes the problem.

Read more comments on GitHub >

github_iconTop Results From Across the Web

A Fluent Interface for JavaScript Promises - Medium
A Fluent Interface is an OOP API using method chaining to increase code legibility. This article explains how to transform JavaScript Promises ...
Read more >
Fluently specifying taint-flow queries with fluentTQL
Instantiations of fluentTQL, on top of two taint analysis solvers, Boomerang and FlowDroid, show and validate fluent TQL expressiveness.
Read more >
Using Method Chaining With The Revealing Module Pattern In ...
A consistent, easy api for both inheritance and instantiation. Want to inherit from n objects? Make a single function call. Want an instance?...
Read more >
Fluent Interfaces - Method Chaining - Stack Overflow
This has two advantages - it keeps the fluent API in one place, ... the object initialization to simulate fluent, but this only...
Read more >
Mockito (Mockito 4.11.0 API) - javadoc.io
Example that shows how deep stub works: Foo mock = mock(Foo.class, RETURNS_DEEP_STUBS); // note that we're stubbing a chain of methods here: getBar()....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found