Skip to content

[Performance] Adding just one more method causes check time to jump from 2s to 100+s #31612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
AnyhowStep opened this issue May 28, 2019 · 15 comments
Assignees
Labels
Bug A bug in TypeScript

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented May 28, 2019

TypeScript Version: 3.5.0-dev.20190523

Search Terms: method, generic, interface, slow check time, this

Code

Scroll to the bottom to see the repository link and reproduction steps.
For now, I'll post a rough example of what I am experiencing,


I first had something like this,

type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;

interface Interface0<T> extends Interface1<T> {
    foo<U> (u : U) : (Baz<Foo<U, this>>);

    //A bunch of other methods.
}

The above compiles in 2s. Nothing suspicious.

Files:           130
Lines:         38454
Nodes:        171047
Identifiers:   56900
Symbols:       54146
Types:         18772
Memory used: 111991K
I/O read:      0.01s
I/O write:     0.03s
Parse time:    0.50s
Bind time:     0.27s
Check time:    1.35s
Emit time:     0.48s
Total time:    2.59s

Then, I added one more method,

type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;

interface Interface0<T> extends Interface1<T> {
    foo<U> (u : U) : (Baz<Foo<U, this>>);

    //The new method
    bar<V> (v : V) : (Baz<Bar<V, this>>);

    //A bunch of other methods.
}

And then it took 102s!

Files:           130
Lines:         38452
Nodes:        171976
Identifiers:   57291
Symbols:      120444
Types:         61118
Memory used: 174232K
I/O read:      0.01s
I/O write:     0.03s
Parse time:    0.52s
Bind time:     0.34s
Check time:  101.11s
Emit time:     0.53s
Total time:  102.51s

This was very suspicious. I looked at the output .d.ts file but there was nothing out of place.
In fact, emit only took 0.53s


I assumed this new method must be the culprit.

However, I decided to comment out everything but the new method,

type Foo<U, Blah> = /* some complicated type */;
type Bar<V, Blah> = /* some complicated type */;
interface Interface1<T> { /* some complicated type */ }
type Baz<B> = /* some complicated type */;

interface Interface0<T> extends Interface1<T> {
    //commented out
    //foo<U> (u : U) : (Baz<Foo<U, this>>);

    //The new method
    bar<V> (v : V) : (Baz<Bar<V, this>>);

    /*
    //commented out
    //A bunch of other methods.
    */
}

This time, it only took 3s.

Files:           130
Lines:         38452
Nodes:        171950
Identifiers:   57280
Symbols:       88571
Types:         38360
Memory used: 126678K
I/O read:      0.01s
I/O write:     0.24s
Parse time:    0.47s
Bind time:     0.26s
Check time:    1.79s
Emit time:     0.71s
Total time:    3.23s

So, the problem wasn't this new method, it seemed.


I was so confused. This didn't make sense.
Then, I got to thinking, "What if it was the number of methods?"
Like, just add one more arbitrary method and see what happens.

Copy+paste foo<>() and rename it to foo2. Change nothing else,

interface Interface0<T> extends Interface1<T> {
    //No longer commented
    foo<U> (u : U) : (Baz<Foo<U, this>>);
    foo2<U> (u : U) : (Baz<Foo<U, this>>);

    //Notice bar<V>() is now deleted

    //No longer commented
    //A bunch of other methods.
}

The above now compiles in 47+s. What?

Files:           130
Lines:         38461
Nodes:        171976
Identifiers:   57291
Symbols:      118614
Types:         57314
Memory used: 139677K
I/O read:      0.01s
I/O write:     0.04s
Parse time:    0.51s
Bind time:     0.29s
Check time:   45.83s
Emit time:     0.56s
Total time:   47.20s

I looked at the generated .d.ts file and I didn't see any explosion of types.
It looked exactly the way I predicted it should and only took up 29 lines.

So, without foo2<>(), it takes 2s. With it, it takes 47s.
And foo2<>() is exactly the same as foo<>()


Copy+paste foo<>() (again) and rename it to foo3. Change nothing else,

interface Interface0<T> extends Interface1<T> {
    foo<U> (u : U) : (Baz<Foo<U, this>>);
    foo2<U> (u : U) : (Baz<Foo<U, this>>);
    foo3<U> (u : U) : (Baz<Foo<U, this>>);

    //A bunch of other methods.
}

I started this build at 22:20, 2019 May 27.
Well, it's been checking for 40 minutes and hasn't finished yet.

Expected behavior:

  • Adding copies of foo<>() without any changes shouldn't increase check time.
  • If bar<>() alone compiles in 3s, and x<>(), y<>(), z<>() compile in 2s, compiling all of these methods should not take 102s.

Actual behavior:

  • Check time is crazy long.
  • It's still checking as of this writing.
  • My laptop, a ThinkPad P52, is making scary exhaust fan noises.
  • My laptop is the hottest it has ever been, I think the CPU might melt.

Github Link:

https://github.com/AnyhowStep/type-mapping/tree/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src

  1. Clone the repository
  2. npm install
  3. npm run build
  4. Grab coffee

The problem is specifically in src/fluent.ts

You'll see derive2<>(), derive3<>(). Uncommented, it'll take forever to check.
https://github.com/AnyhowStep/type-mapping/blob/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src/fluent.ts#L64

You'll see rename<>(). Uncommented, it'll take 100s to check.
But if you comment everything else but rename<>(), it'll take 3s to check.
https://github.com/AnyhowStep/type-mapping/blob/d09851dc437aa25b702a27fa7b6eb5060dd476c5/src/fluent.ts#L86

Related Issues:

I know there are a bunch of performance related issues around here.
I've even opened a few myself in the past.
But I'm not even sure what is related to this.


As a side note, I usually implement my libraries with just a function composition API.
Then, when I want to make it more usable, I add a fluent API on top of it.

But it seems like, with TypeScript, whenever I start to build the fluent API wrapper, I get weird problems like reaching the max instantiation depth, or super-long check times.

I guess function composition > fluent?


I feel like this problem might have to do with the fact that it's a generic interface and the return type of the methods reference the this type.


This reminds me of TREE(3).

  • TREE(1) = 1
  • TREE(2) = 3
  • TREE(3) > googolplex

Except,

  • foo(1) = 2
  • foo(2) = 47
  • foo(3) > TREE(3) (Proof is left as an exercise to the reader)
@fatcerberus
Copy link

  • TREE(1) = 1
  • TREE(2) = 3
  • TREE(3) > googolplex

What. Why is this a thing.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 28, 2019

https://www.youtube.com/watch?v=3P6DWAwwViU

I love this.

TL;DW,

There's this game where you draw "trees" to build a "forest". Each "tree" is made up of colored nodes, and non-colored edges. The game ends when certain conditions are met.

TREE(1) is the most number of "trees" you can draw using 1 color.
TREE(2) is the most number of "trees" you can draw using 2 colors.

It just so happens that when you have 3 colors, you can draw a tonne of different "trees" without causing the game to end.


Also, tsc is still running.

@fatcerberus
Copy link

Also, tsc is still running.

At this point I'm going to go out on a limb and guess that the compiler actually got caught in an infinite loop (of course I can't prove that--halting problem and all that). Why that is... I couldn't say.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 28, 2019

I just decided to kill it.
image

About a minute after killing it,
image

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 28, 2019

I decided to tweak the declaration a little.

To use type I<F> = F & { /* blah */ } instead of using an interface.

https://github.com/AnyhowStep/type-mapping/tree/ab2614abc5ee05ba4edfb863a17192b98af8a8bd

Under src/fluent-2.ts,
https://github.com/AnyhowStep/type-mapping/blob/ab2614abc5ee05ba4edfb863a17192b98af8a8bd/src/fluent-2.ts

The method declarations no longer take forever to check.
However, when I try to implement it, I get performance problems again.


Commenting out the as any parts around this line will cause long check times,
https://github.com/AnyhowStep/type-mapping/blob/ab2614abc5ee05ba4edfb863a17192b98af8a8bd/src/fluent-2.ts#L181

It should be assignable but the checker seems to have problems with it.


This line is meant to cause a compile error.
https://github.com/AnyhowStep/type-mapping/blob/ab2614abc5ee05ba4edfb863a17192b98af8a8bd/src/fluent-2.ts#L197

However, the error message is really long.
Nothing wrong with it. Just thought it was amusing.


With interfaces,

  • Declarations take forever to check
  • Implementations are fine

With type aliases,

  • Declarations are fine
  • Implementations take forever to check

@RyanCavanaugh
Copy link
Member

Is there an isolated version of this we can pull?

@fatcerberus
Copy link

I think there’s a link in the OP.

@RyanCavanaugh
Copy link
Member

"Isolated" meaning a single file of code

https://twitter.com/SeaRyanC/status/1105951874623004672

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 28, 2019

Oh, sorry, I just saw this. I'll try and get to it. Not sure if it's worth noting at all but the repo has no external dependencies during run time. And doesn't use node libraries.

No need to worry about malicious code executing

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 28, 2019

With this,

Playground

type UnionToIntersection<U> = (
    (
        U extends any ? (k: U) => void : never
    ) extends (
        (k: infer I) => void
    ) ? I : never
);

interface Mapper<HandledInputT, OutputT> {
    (name : string, mixed : HandledInputT) : OutputT,
}
type AnyMapper = (
    Mapper<any, any>
);
type SafeMapper<OutputT> = (
    Mapper<unknown, OutputT>
);

type AnySafeMapper = (
    SafeMapper<any>
);
interface ExpectedInput<T> {
    __expectedInput? : [T],
}
interface MappableInput<T> {
    __mappableInput? : [T],
}
type OutputOf<F extends AnyMapper> = (
    ReturnType<F>
);
type ExpectedInputOfImpl<F extends AnyMapper> = (
    F extends Mapper<infer T, any> ?
    (
        unknown extends T ?
        (
            F extends ExpectedInput<infer T> ?
            [T] :
            F extends MappableInput<infer T> ?
            [T] :
            [OutputOf<F>]
        ) :
        [T]
    ) :
    never
);
type ExpectedInputOf<F extends AnyMapper> = (
    Extract<
        UnionToIntersection<ExpectedInputOfImpl<F>>,
        [any]
    >[0]
);
type MappableInputOfImpl<F extends AnyMapper> = (
    F extends Mapper<infer T, any> ?
    (
        unknown extends T ?
        (
            F extends MappableInput<infer T> ?
            [T] :
            F extends ExpectedInput<infer T> ?
            [T] :
            [OutputOf<F>]
        ) :
        [T]
    ) :
    never
);
type MappableInputOf<F extends AnyMapper> = (
    Extract<
        UnionToIntersection<MappableInputOfImpl<F>>,
        [any]
    >[0]
);
interface Optional {
    __optional : true,
}
type IsOptional<F extends AnySafeMapper> = (
    F extends Optional ?
    (
        undefined extends MappableInputOf<F> ?
        true :
        false
    ) :
    false
);

type IsExpectedInputOptional<F extends AnySafeMapper> = (
    IsOptional<F> extends true ?
    (
        undefined extends ExpectedInputOf<F> ?
        true :
        false
    ) :
    false
);

type DeriveMapper<
    SrcKeyT extends string,
    DstKeyT extends string,
    F extends AnySafeMapper
> = (
    & SafeMapper<
        { [dst in DstKeyT] : OutputOf<F> }
    >
    & ExpectedInput<
        IsExpectedInputOptional<F> extends true ?
        { [src in SrcKeyT]? : ExpectedInputOf<F> } :
        { [src in SrcKeyT] : ExpectedInputOf<F> }
    >
    & MappableInput<
        IsOptional<F> extends true ?
        { [src in SrcKeyT]? : MappableInputOf<F> } :
        { [src in SrcKeyT] : MappableInputOf<F> }
    >
);
type RenameMapper<
    SrcKeyT extends string,
    DstKeyT extends string,
    F extends AnySafeMapper
> = (
    & SafeMapper<
        { [dst in DstKeyT] : OutputOf<F> }
    >
    & ExpectedInput<
        IsExpectedInputOptional<F> extends true ?
        { [dst in DstKeyT]? : ExpectedInputOf<F> } :
        { [dst in DstKeyT] : ExpectedInputOf<F> }
    >
    & MappableInput<
        IsOptional<F> extends true ?
        (
            | { [src in SrcKeyT]? : MappableInputOf<F> }
            | { [dst in DstKeyT]? : MappableInputOf<F> }
        ) :
        (
            | { [src in SrcKeyT] : MappableInputOf<F> }
            | { [dst in DstKeyT] : MappableInputOf<F> }
        )
    >
);

type FluentMapper<F extends AnySafeMapper> = (
    & IFluentMapper<F>
    & F
);
interface IFluentMapper<F extends AnySafeMapper> {
    derive<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );

    //Uncomment this block to warm yourself up during winter
    /*
    derive2<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
    //*/
    //Uncomment this block to warm yourself up during winter
    /*
    derive3<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
    //*/

    rename<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<RenameMapper<SrcKeyT, DstKeyT, F>>
    );
}

declare function derive<
    SrcKeyT extends string,
    DstKeyT extends string,
    F extends AnySafeMapper
> (
    srcKey : SrcKeyT,
    dstKey : DstKeyT,
    f : F
) : (
    DeriveMapper<SrcKeyT, DstKeyT, F>
);
/**
    MODIFIES THE FUNCTION `f`!
    DOES NOT RETURN A NEW FUNCTION.
*/
function fluentMapper<F extends AnySafeMapper> (f : F) : FluentMapper<F> {
    const result : FluentMapper<F> = f as any;

    result.derive = <
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    ) => {
        return fluentMapper(derive(srcKey, dstKey, f));
    };
    return result;
}
Files:           131
Lines:         38697
Nodes:        172665
Identifiers:   57565
Symbols:       86861
Types:         38338
Memory used: 138352K
I/O read:      0.01s
I/O write:     0.04s
Parse time:    0.49s
Bind time:     0.30s
Check time:    6.83s
Emit time:     0.49s
Total time:    8.11s

With derive2<>() uncommented,

Playground

Files:           131
Lines:         38697
Nodes:        172692
Identifiers:   57577
Symbols:       87697
Types:         39217
Memory used: 199515K
I/O read:      0.01s
I/O write:     0.04s
Parse time:    0.47s
Bind time:     0.31s
Check time:   30.57s
Emit time:     0.50s
Total time:   31.85s

With derive2<>() and derive3<>() uncommented,

Playground

Very long

With derive2<>() and derive3<>() uncommented,

And,

    result.derive = (<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    ) => {
        return fluentMapper(derive(srcKey, dstKey, f));
    });

Changed to,

    result.derive = (<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    ) => {
        return fluentMapper(derive(srcKey, dstKey, f)) as any;
    }) as any;

Playground

Files:           131
Lines:         38697
Nodes:        172724
Identifiers:   57589
Symbols:       84848
Types:         36165
Memory used: 127911K
I/O read:      0.01s
I/O write:     0.04s
Parse time:    0.48s
Bind time:     0.30s
Check time:    6.13s
Emit time:     0.50s
Total time:    7.41s

So, we have two as any assertions, and derive2<>() and derive3<>() uncommented right now.
If we remove either one, or remove both of the as any assertions, it'll take forever to check again.

I guess it has trouble realizing,

  • fluentMapper(derive(srcKey, dstKey, f)) is assignable to FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
  • Also,
      <
          SrcKeyT extends string,
          DstKeyT extends string
      > (
          srcKey : SrcKeyT,
          dstKey : DstKeyT
      ) => (
          FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
      )
    
    is assignable to FluentMapper<F>["derive"]

If you have,

  • derive2<>() uncommented
  • derive3<>() commented
  • One or both as any removed

It'll take 31s.


  • derive2<>() uncommented
  • derive3<>() uncommented
  • rename<>() commented
  • One or both as any removed

It'll take 20s.

Playground


  • derive2<>() uncommented
  • derive3<>() commented
  • rename<>() commented
  • One or both as any removed

It'll take 7s.

Playground


If you have,

  • derive2<>() uncommented
  • derive3<>() uncommented
  • derive4<>() uncommented
  • rename<>() commented
  • One or both as any removed

It'll take.... Very long. I got to play an entire song (3m28s) and it still didn't complete.

Playground

Also, CPU started to go crazy,

image

So, it seems like rename<>() isn't even really needed to show there's a problem somewhere.

Just having enough copies of derive<>() freaks it out. This kind of scares me because I don't know if the next method I add to the interface will cause check times to jump to impossibly long times.

@AnyhowStep
Copy link
Contributor Author

Okay, even shorter repro, removing rename<>(),

Playground

type UnionToIntersection<U> = (
    (
        U extends any ? (k: U) => void : never
    ) extends (
        (k: infer I) => void
    ) ? I : never
);

interface Mapper<HandledInputT, OutputT> {
    (name : string, mixed : HandledInputT) : OutputT,
}
type AnyMapper = (
    Mapper<any, any>
);
type SafeMapper<OutputT> = (
    Mapper<unknown, OutputT>
);

type AnySafeMapper = (
    SafeMapper<any>
);
interface ExpectedInput<T> {
    __expectedInput? : [T],
}
interface MappableInput<T> {
    __mappableInput? : [T],
}
type OutputOf<F extends AnyMapper> = (
    ReturnType<F>
);
type ExpectedInputOfImpl<F extends AnyMapper> = (
    F extends Mapper<infer T, any> ?
    (
        unknown extends T ?
        (
            F extends ExpectedInput<infer T> ?
            [T] :
            F extends MappableInput<infer T> ?
            [T] :
            [OutputOf<F>]
        ) :
        [T]
    ) :
    never
);
type ExpectedInputOf<F extends AnyMapper> = (
    Extract<
        UnionToIntersection<ExpectedInputOfImpl<F>>,
        [any]
    >[0]
);
type MappableInputOfImpl<F extends AnyMapper> = (
    F extends Mapper<infer T, any> ?
    (
        unknown extends T ?
        (
            F extends MappableInput<infer T> ?
            [T] :
            F extends ExpectedInput<infer T> ?
            [T] :
            [OutputOf<F>]
        ) :
        [T]
    ) :
    never
);
type MappableInputOf<F extends AnyMapper> = (
    Extract<
        UnionToIntersection<MappableInputOfImpl<F>>,
        [any]
    >[0]
);
interface Optional {
    __optional : true,
}
type IsOptional<F extends AnySafeMapper> = (
    F extends Optional ?
    (
        undefined extends MappableInputOf<F> ?
        true :
        false
    ) :
    false
);

type IsExpectedInputOptional<F extends AnySafeMapper> = (
    IsOptional<F> extends true ?
    (
        undefined extends ExpectedInputOf<F> ?
        true :
        false
    ) :
    false
);

type DeriveMapper<
    SrcKeyT extends string,
    DstKeyT extends string,
    F extends AnySafeMapper
> = (
    & SafeMapper<
        { [dst in DstKeyT] : OutputOf<F> }
    >
    & ExpectedInput<
        IsExpectedInputOptional<F> extends true ?
        { [src in SrcKeyT]? : ExpectedInputOf<F> } :
        { [src in SrcKeyT] : ExpectedInputOf<F> }
    >
    & MappableInput<
        IsOptional<F> extends true ?
        { [src in SrcKeyT]? : MappableInputOf<F> } :
        { [src in SrcKeyT] : MappableInputOf<F> }
    >
);

type FluentMapper<F extends AnySafeMapper> = (
    & IFluentMapper<F>
    & F
);
interface IFluentMapper<F extends AnySafeMapper> {
    derive<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );

    //Uncomment this block to warm yourself up during winter
    //*
    derive2<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
    //*/
    //Uncomment this block to warm yourself up during winter
    //*
    derive3<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
    //*/
    //Uncomment this block to warm yourself up during winter
    //*
    derive4<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
    //*/
}

declare function derive<
    SrcKeyT extends string,
    DstKeyT extends string,
    F extends AnySafeMapper
> (
    srcKey : SrcKeyT,
    dstKey : DstKeyT,
    f : F
) : (
    DeriveMapper<SrcKeyT, DstKeyT, F>
);
/**
    MODIFIES THE FUNCTION `f`!
    DOES NOT RETURN A NEW FUNCTION.
*/
function fluentMapper<F extends AnySafeMapper> (f : F) : FluentMapper<F> {
    const result : FluentMapper<F> = f as any;

    //Comment out the `as any` to see it freak out.
    result.derive = (<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    ) => {
        return fluentMapper(derive(srcKey, dstKey, f)) /**/as any;
    }) /**/as any;
    return result;
}
Files:           131
Lines:         38675
Nodes:        172620
Identifiers:   57549
Symbols:       84830
Types:         36126
Memory used: 128812K
I/O read:      0.01s
I/O write:     0.03s
Parse time:    0.45s
Bind time:     0.28s
Check time:    5.84s
Emit time:     0.48s
Total time:    7.04s

Comment out both as any parts,

Playground

Now, it takes forever.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented May 29, 2019

If I change,

type FluentMapper<F extends AnySafeMapper> = (
    & IFluentMapper<F>
    & F
);

to,

type FluentMapper<F extends AnySafeMapper> = (
    & IFluentMapper<F>
    //& F
);

It checks in 7s again. No clue why.

So, multiple copies of derived<>() + & F = bad.

@AnyhowStep
Copy link
Contributor Author

I decided to forgo the & F part and just return a wrapper,

export interface FluentMapper<F extends AnySafeMapper> {
    (name : string, mixed : unknown) : OutputOf<F>;

    __expectedInput? : [ExpectedInputOf<F>];
    __mappableInput? : [MappableInputOf<F>];
    __optional : F extends Optional ? true : false;

    //== object ==

    derive<
        SrcKeyT extends string,
        DstKeyT extends string
    > (
        srcKey : SrcKeyT,
        dstKey : DstKeyT
    ) : (
        FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>>
    );
}

Now, I can have as many copies of derive<>() as I want and it checks in 2s.

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Jun 13, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.6.0 milestone Jun 13, 2019
@weswigham
Copy link
Member

weswigham commented Jun 18, 2019

In this example the types under consideration are.... complex:

image

What seems to be going on is that every FluentMapper<DeriveMapper<SrcKeyT, DstKeyT, F>> we encounter as we descend through the comparison expands - namely it becomes a IFluentMapper<T> & T - and since there's no "base case" there as we keep descending on generic types - we'll look at a IFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & T, then a IFluentMapper<IFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & T> & IFluentMapper<IFluentMapper<T> & T> & IFluentMapper<T> & T and so on until we hit a depth limiter, then crawl back out and continue down another branch doing the same. That depth limit is pretty high, and gets expontentiated in comparison time when the comparison is re-performed multiple times with different type parameters (as each copy of derived has unique type parameters).

Fixing this may require something similar to an insight I has while working on #31633 - namely that while we're measuring weather IFoo<T> is assignable to IFoo<U>, asking if IFoo<Q> is assignable to IFoo<J> should already be assumed to be true, assuming Q is related to J as T is related to U.

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.8.1 milestone Jan 8, 2020
@weswigham
Copy link
Member

@AnyhowStep nowadays we issue a Type instantiation is excessively deep and possibly infinite. error and terminate pretty quickly on the code given here, yeah? So this has been "fixed" by one of our other limiters - at least we acknowledge our own limitations now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants