Skip to content
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

Feature request: lift the circular constraint for conditional types #26980

Closed
4 tasks done
dead-claudia opened this issue Sep 8, 2018 · 29 comments · Fixed by #40002
Closed
4 tasks done

Feature request: lift the circular constraint for conditional types #26980

dead-claudia opened this issue Sep 8, 2018 · 29 comments · Fixed by #40002
Labels
Fix Available A PR has been opened for this issue Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@dead-claudia
Copy link

dead-claudia commented Sep 8, 2018

Search Terms

circular type conditional type

Suggestion

Currently, Last1 is valid, but seemingly-equivalent Last2 is not:

type Head<T extends any[]> = T extends [infer X, ...any[]] ? X : never;

type Tail<T extends any[]> =
    ((...x: T) => void) extends ((x: any, ...xs: infer XS) => void) ? XS : never;

type Last1<T extends any[]> = {
  0: never,
  1: Head<T>,
  2: Last1<Tail<T>>,
}[T extends [] ? 0 : T extends [any] ? 1 : 2];

type Last2<T extends any[]> =
    T extends [] ? never :
    T extends [infer R] ? R :
    Last2<Tail<T>>;

My suggestion is to make Last2 valid, since the recursion isn't necessarily unbounded.

Use Cases

It'd make recursive conditional types a lot easier and more intuitive to write. It'd also eliminate the need to guard against impossible cases when using the workaround like when using Last1 above. If you wanted to mandate termination, all I'd want is direct recursion - this makes it much easier to check for infinite recursion. (Other types are generally assumed to terminate, so you're only assessing control flow.)

Examples

See above in the suggestion summary. Here's a concrete example of how this file (with dependency) would be simplified:

type Last<L extends any[], D = never> =
    L extends [] ? D :
    L extends [infer H] ? H :
    ((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? Last<T> :
    D;

type Append<T extends any[], H> =
    ((h: H, ...t: T) => any) extends ((...l: infer L) => any) ? L : never;

type Reverse<L extends any[], R extends any[] = []> =
    ((...l: L) => any) extends ((h: infer H, ...t: infer T) => any) ?
        Reverse<T, Append<R, H>> :
        R;

type Compose<L extends any[], V, R extends any[] = []> =
    ((...l: L) => any) extends ((a: infer H, ...t: infer T) => any) ?
        Compose<T, H, Append<R, (x: V) => H>> :
        R;

export type PipeFunc<T extends any[], V> =
    (...f: Reverse<Compose<T, V>>) => ((x: V) => Last<T, V>);

Currently, you could achieve similar via this, but as you can see, it's highly repetitive and boilerplatey:

type Last<L extends any[], D = never> = {
    0: D,
    1: L extends [infer H] ? H : never,
    2: ((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? Last<T> : D,
}[L extends [] ? 0 : L extends [any] ? 1 : 2];

type Append<T extends any[], H> =
    ((h: H, ...t: T) => any) extends ((...l: infer L) => any) ? L : never;

type Reverse<L extends any[], R extends any[] = []> = {
    0: R,
    1: ((...l: L) => any) extends ((h: infer H, ...t: infer T) => any) ?
        Reverse<T, Append<R, H>> :
        never,
}[L extends [any, ...any[]] ? 1 : 0];

type Compose<L extends any[], V, R extends any[] = []> = {
    0: R,
    1: ((...l: L) => any) extends ((a: infer H, ...t: infer T) => any) ?
        Compose<T, H, Append<R, (x: V) => H>>
        : never,
}[L extends [any, ...any[]] ? 1 : 0];

export type PipeFunc<T extends any[], V> =
    (...f: Reverse<Compose<T, V>>) => ((x: V) => Last<T, V>);

(The original code snippet is linked to from here as a possible solution to the issue of _.compose, _.flow, and friends.)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@dead-claudia
Copy link
Author

dead-claudia commented Sep 8, 2018

BTW, here's the Wikipedia article on total functional programming, where TS's type system models to a very large extent in my experience (probably unintentionally). And yes, this could enable a type-encoded quicksort across integers and potentially strings if they were made comparable, without even requiring the type system to remain Turing-complete.


After including this, you could then permanently break the Turing-complete aspect by making a similar check with indexed types. Note that AFAICT, the Turing-complete nature is dependent on indexed types not being checked for recursive cases. It may require support for corecursive functions, depending on how much code breaks.

@dead-claudia
Copy link
Author

Oh, and another reason to address Turing completeness and potentially kill it is because the original "solution" to variadic composition happens to hang when plugged into the playground, due to a bug not very clear when just looking. It maxes out my computer's CPU without taking much memory at all, which is a pretty obvious problem, but it can happen when a type system is Turing-complete, but not sound enough to force you to be explicit about your intent (you see similar with C-like procedural languages and infinite loops with non-trivial logic, hence various control-flow-sensitive lints for it).

@jcalz
Copy link
Contributor

jcalz commented Sep 14, 2018

(Moving the conversation from #5453)

Regarding the Last1<> implementation, the official word is something like "don't do it". @ahejlsberg said:

It's clever, but it definitely pushes things well beyond their intended use. While it may work for small examples, it will scale horribly. Resolving those deeply recursive types consumes a lot of time and resources and might in the future run afoul of the recursion governors we have in the checker.

Don't do it!

I would be enormously in favor of something that lets me express more recursive type functions without running afoul of the current pitfalls (warnings, hanging compiler, crashing compiler). Not sure how to get there from here, though. I suspect if the circularity warning were simply removed, the Last2<> implementation would bog down the compiler in the same way that Last1<> does. Assuming that the type checker sometimes treats T extends U ? X : Y like X | Y for testing assignability, any recursion that depends on conditional types to express termination could be a problem.

I'm not holding my breath for any solution involving a significant overhaul of the type checker. Can we think of some addition which wouldn't break existing code but would allow some (bounded) type function iteration/recursion?

@jcalz
Copy link
Contributor

jcalz commented Nov 14, 2018

Copying from the Design Notes for ease of linking.

@DanielRosenwasser said:

Recursive Types

  • The intuition is not possible to form object graphs with circularities unless you defer somehow (either via laziness or state).
  • Really there's no way to know if any arbitrary type will terminate.
  • We can have limited types of recursion in the compiler, but the problem isn't whether or not the type terminates, it's how computationally intensive and memory heavy the resolution is.
  • We have a meta-question: do we want people writing code like this?
    • The motivating scenarios that people have are legitimate, but achieving them with these types is not necessarily going to work well for library consumers.
  • Conclusion: we're not quite ready for this sort of thing.

@dead-claudia
Copy link
Author

dead-claudia commented Mar 28, 2019

May I add another compelling use case: you could lift the similar constraints on unions, intersections, and generic type parameters to address the possible results of JSON.parse(string) without a reviver:

type JSONResult = null | string | boolean | number | Record<string, JSONResult> | JSONResult[];

In this case, JSONResult can only really be these types, and anything else is entirely unreasonable. You can detect the safety and legality by ensuring that any cycles are abstracted through an object property of some kind. In the case of Record<string, JSONResult>, it's a [key: string] property, and in the case of JSONResult[], it's a [key: number] property.

Edit: Missed a type.

@jcalz jcalz mentioned this issue May 1, 2019
5 tasks
@fightingcat
Copy link

type Last<T extends any[]> = T[Tail<T>['length']];

@jcalz
Copy link
Contributor

jcalz commented Jul 2, 2019

cross-linking #30188

And calling @pirix-gh... I think since you are using recursive conditional types inside ts-toolbelt and committed them into the typings for ramda it would be useful for someone to either reiterate that this is a Bad Idea, or give some update if there is one.

And if there ever is an update here past "don't do it" and "we're not ready for this", I'd expect to see some recursive conditional types included in the baseline tests for the language itself, so that future releases won't break them.

@millsp
Copy link
Contributor

millsp commented Jul 2, 2019

Quotes from #5453 (comment)

Can we discuss whether we can have official approval for the types provided by the ts-toolbelt or not?
Here's how the story started:

Performance
I created a heavy test that shows the Minus type in action. It performs the Minus 216000 times in less than 4 seconds. This shows that TS can handle recursive types very well. But this is quite recent.

Since when?
This is thanks to Anders 🎉 (#30769). He allowed me to switch from conditional types to indexed conditions (like a switch). And as a matter of fact, it improved performance by x6 (with everything being indexed).

Is it safe?
The lib also makes recursion safe with Iteration that will prevent any overflow from TypeScript. In other words, if I goes over 40 then it overflows and Pos<I> equals number. Thus stopping the recursion safely. Here's an example:

import {O, I, T} from 'ts-toolbelt'

// It works with the same principles `Minus` uses
type Assign<O extends object, Os extends object[], I extends I.Iteration = I.IterationOf<'0'>> = {
    0: Assign<O.Merge<Os[I.Pos<I>], O>, Os, I.Next<I>>
    1: O
}[
    I.Pos<I> extends T.Length<Os>  
    ? 1
    : 0
]

type test0 = Assign<{i: number}, [
    {a: '1', b: '0'},
    {a: '2'},
    {a: '3', c: '4'},
]>

Thoughts

  • Even though it is possible to write optimized recursive types, it requires being rigorous and it should not be approached by inexperienced ones that could write unsafe (heavy) types. This is why I created this repository to ensure the quality of the types.
  • I did a test of 12 960 000 Minus operations to calculate the average cost of Minus. It took 125 seconds to complete the compilation. So a single Minus operation (~30 iterations) takes 9.65e-6 seconds to be performed.
  • The library itself has a very low overhead (+2 seconds)
  • This could be great for the future of TS, as we could write more features with the type system itself. Thus relieving the TS team from the constant demand for new features (at least partially).
  • This is not about lifting circular constraints as they clearly signal problems. It's about approving or not types like the one above (which are able not to overflow).

What now?

  • Can we debate about the pros/cons to make this ts-toolbelt approved or not?
  • Can we talk about integrating such support into the TS project (like @jcalz asked)?
  • Could TS be optimized to provide even better support? Here's some thoughts:
    • The compiler could compile the types themselves, thus saving that compilation time on shared packages. Basically, we don't want to recompute a type (like Minus) every time, we could just compute its output once, and this is what would end up in a generated d.ts. This could (maybe) be determined whether a type is waiting for arguments (can't be computed) or is argument-less (can be computed).
    • TypeScript could maybe avoid computing types that require arguments until they are provided with them. This could help with performance as TS calculates all the possible outputs of a type that has generics. I especially think of mapped types that already have such a behavior.
  • Should I open a proposal?

@ahejlsberg @RyanCavanaugh @DanielRosenwasser

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 26, 2019

@jcalz Circular conditional type police? I would like to turn myself in,

I just wrote a generic compose() function with no overloads,

//(a: number, b: string) => Date | null
const many = compose(
    (a : number, b : string) => a+b,
    (s : string) => s.length > 2,
    (b : boolean) => Math.random() > 0.5 && b ? new Date() : null
);

interface CompileError<_ErrorMessageT> {
    __compileError : never
}
type PopFront<ArrT extends readonly any[]> = (
    ((...arr : ArrT) => void) extends ((head : any, ...tail : infer TailT) => void) ?
    TailT :
    never
);
type PushFront<TailT extends readonly any[], HeadT> = (
    ((head : HeadT, ...tail : TailT) => void) extends ((...arr : infer ArrT) => void) ?
    ArrT :
    never
);
//////////////////////////////////
type AssertCanComposeImpl<
    F extends (...args : any[]) => any,
    ArrT extends readonly ((arg : any) => any)[],
    IndexT extends any[]
> = (
    {
        0 : unknown,
        1 : (
            [ReturnType<F>] extends Parameters<ArrT[0]> ?
            AssertCanComposeImpl<
                ArrT[0],
                PopFront<ArrT>,
                PushFront<IndexT, any>
            > :
            CompileError<[
                "Output",
                [ReturnType<F>],
                "of function",
                IndexT["length"],
                "cannot be input to function taking",
                Parameters<ArrT[0]>
            ]>
        )
    }[
        ArrT["length"] extends 0 ?
        0 :
        1
    ]
);
type AssertCanCompose<
    F extends (...args : any[]) => any,
    ArrT extends readonly ((arg : any) => any)[]
> = (
    number extends ArrT["length"] ?
    unknown :
    AssertCanComposeImpl<
        F,
        ArrT,
        []
    >
);
//////////////////////////////////
type ComposeImpl<
    F extends (...args : any[]) => any,
    ArrT extends readonly ((arg : any) => any)[],
    ParametersT extends any[]
> = (
    {
        0 : (...args : ParametersT) => ReturnType<F>,
        1 : (
            [ReturnType<F>] extends Parameters<ArrT[0]> ?
            ComposeImpl<
                ArrT[0],
                PopFront<ArrT>,
                ParametersT
            > :
            never
        )
    }[
        ArrT["length"] extends 0 ?
        0 :
        1
    ]
);
type Compose<
    F extends (...args : any[]) => any,
    ArrT extends readonly ((arg : any) => any)[]
> = (
    number extends ArrT["length"] ?
    (...args : Parameters<F>) => unknown :
    ComposeImpl<
        F,
        ArrT,
        Parameters<F>
    >
);
declare function compose<
    P extends any[],
    R extends any,
    ArrT extends readonly ((arg : any) => any)[]
> (
    f : (
        (...args : P) => (
            & R
            & AssertCanCompose<
                () => R,
                ArrT
            >
        )
    ),
    ...arr : ArrT
) : (
    Compose<(...args : P) => R, ArrT>
);

//Error: Expected at least 1 arguments, but got 0.
compose();

//(a: number, b: string) => unknown
const doNotKnowReturnType = compose(
    (a : number, b : string) => a+b,
    ...[]
);

//(a: number, b: string) => string
const one = compose(
    (a : number, b : string) => a+b
);

//(a: number, b: string) => Date | null
const many = compose(
    (a : number, b : string) => a+b,
    (s : string) => s.length > 2,
    (b : boolean) => Math.random() > 0.5 && b ? new Date() : null
);

//const cannotCompose: never
//
const cannotCompose = compose(
    /*
        CompileError<[
            "Output",
            [Date],
            "of function", 1,
            "cannot be input to function taking",
            [boolean]
        ]>
    */
    (a : number, b : string) => a+b,
    (s : string) => new Date(), //Uh oh, next function expects boolean
    (b : boolean) => Math.random() > 0.5 && b ? new Date() : null
);

Playground


[Edit]

Just realized I left in the unnecessary IndexT. It was there because the assertion and actual composition were one type before. I split it into two types and forgot to clean up


[Edit]

Removed IndexT from ComposeImpl<>

@millsp
Copy link
Contributor

millsp commented Jul 26, 2019

@AnyhowStep I only use recursion in cases where the length of a tuple changes. For all the rest (in most cases) you should be able to use mapped types (it applies in this case). You can check out my implementation of compose:

https://github.com/pirix-gh/ts-toolbelt/blob/master/src/Function/Compose.ts

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 26, 2019

Someone on the TS gitter was asking for a compose function signature with arbitrary amounts of functions as inputs. So, I thought it was an interesting request.

I didn't want to just post the answer on gitter and let the code disappear into the ether, though :x

I gave your impl a brief look.

Looks like mine composes forward and yours composes backwards?

Also, mine checks if the output of function N can be an input to function N+1 and gives an appropriate compile error (if a tuple is inferred)

I think your Composer type can easily be made to check input and output types, though.


Also, if TS didn't have that max depth limit, my impl would have arbitrary length support.

But, realistically, since we do have the limit, it craps out after about 21 functions.

I think yours supports up to 40? 50? I forgot how many your Next type supports

If we just use your Compose type, though, it supports the max tuple length TS supports. But we lose the output-input type checks

@millsp
Copy link
Contributor

millsp commented Jul 26, 2019

It composes forward, as it's a mapped type, but it will block at 40 yhea. And it does check for input-output, what do you mean?

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 26, 2019

I was referring to this part,


        CompileError<[
            "Output",
            [Date],
            "of function", 1,
            "cannot be input to function taking",
            [boolean]
        ]>

If they call the compose() function and the output of N doesn't feed into function N+1, it gives a compile-time error


A little out of topic but variadic type params seem less useful with tuples and recursive conditional types

@millsp
Copy link
Contributor

millsp commented Dec 13, 2019

Pinging everyone following this. A PR allowing recurive types has been accepted by the TypeScript team and is now part of their tests. So we can now write such recursive types, at the cost of using a library.

#33810 shows how they work in their most basic form. I recommend you use this as a template. If the template does not fit, make sure your recursive types have proper terminating conditions.

For obvious reasons, the TypeScript team is not responsible for maintaining such techniques, this relies on me. But rest assured, this is well tested as it is part of ramda as well, it is used by many people.

I'm not sure this can close this issue, but is nevertheless a great step forward.

@AnyhowStep
Copy link
Contributor

@pirix-gh Can you toss in type-level generator trampolines to those tests, too? =P

@millsp
Copy link
Contributor

millsp commented Dec 13, 2019

I'd rather love to see type providers one day, to have top performance types.

@jcalz
Copy link
Contributor

jcalz commented Dec 14, 2019

Is there any way to get a version of this that works in the Playground? Or some guidance on how to do this without using an external library? Like maybe we can make a self-contained "ts-toolbelt lite" that can be used inline as opposed to in a module hierarchy? I'm hoping to minimize Stack Overflow and GitHub issue headaches.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 14, 2019

@sandersn if I made a PR to add trampolines to the tests, would it be accepted?

The following file gives an example of what the test would look like,
https://github.com/AnyhowStep/tsql/blob/master/src/tuple-util/reverse.ts

It doesn't rely on external dependencies to work and can easily go beyond 1000+ iterations/recursive calls


[Edit]

Trampolines saved my life with #33541

#33541 (comment)

Another example of trampolines for recursive types,
https://github.com/AnyhowStep/tsql/pull/65/files#diff-0214b2fefabaf1617e690660683db775R226-R237

@millsp
Copy link
Contributor

millsp commented Dec 14, 2019

@jcalz, I'm working on a script that will make it work on the playground (flattening), will ping when it's done

@jack-williams
Copy link
Collaborator

There is the possibility that the current implementation relies on variance markers for conditional types, which are known to have bugs. When I've looked at fixing these issues I've run into problems where conditional types that were checked trivially now fail because they are checked structurally.

For instance, when investigating #31277 I got an infinite type error in something originating in ts-toolbelt. This was pre-3.7, and I haven't looked at how that affects things yet.

When I get a chance I'll see if the changes in #31277 caues any regressions in #33810.

@sandersn
Copy link
Member

Erm, I was the one who merged that test. My intent in adding it to the test suite was not, not to support recursive types. It was to let us (and anybody who wants to follow our user tests) know when we break the latest workarounds. That is, we'll consider that test passing even if there are compilation errors or OOMs.

We have a recursive type limit for a reason — people don't expect a compiler to do arbitrary computation, so compile-time programs that you write need to run fast. This is especially true for commonly-used libraries.

@AnyhowStep That particular test is for ts-toolbelt. Is ts-toolbelt trampolined? If not, I'm fine with adding a separate test in index.ts. @pirix-gh is that OK with you?

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 16, 2019

I don't think ts-toolbelt is trampolined. I can say I have not noticed a negative impact on performance due to trampolines, though.

Trampolines can be an important workaround, even when recursive types are not deeply nested. In the above linked issue (#33541), I only had a recursive type doing two iterations and TS still gave up (unless assigning the result to a temporary variable).

With the trampoline workaround, it works fine with 60+ iterations, whether I assign to a temporary variable or not.

I can make a separate PR that tests trampolines. I didn't even realize it was a thing that could be OK'd

@millsp
Copy link
Contributor

millsp commented Dec 16, 2019

Nope, it's not trampolined, one of my goals is keep ts' memory as sane as I can. Recursive types do get limited by the compiler when they get too complex internally, within themselves (instantiation depth). What I do for recursive types, and that is what @sandersn means by workaround, is that I use:

type RecursiveType<A, B...> =
	_RecursiveType<A, B...> extends infer X
	? X
	: never

This is in a sense a mini-trampoline, to allow combining recursive types together without hitting that instantiation depth error (buildup from the recursive type). I feel like this is already asking a lot from ts. But if you remove the extends infer X part you are left with a fully supported type, as it uses no trick IMHO... However, if I were to remove this on the ts-toolbelt, types like Curry would certainly not be possible.

So I use extends infer X to ask ts to give me more, and is optional when you write your own types.

(Just to give an idea, ts-toolbelt standalone weighs ~40MB, and with all its tests, it climbs up to ~90MB).

@millsp
Copy link
Contributor

millsp commented Dec 16, 2019

I think trampolines belongs to a separate PR. I'd be interested to see memory & performance on them @AnyhowStep.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 16, 2019

I've seen that infer X trick fail in many (of my) use cases. Which is why I opt for a full trampoline nowadays.

import {Reverse} from "./reverse";

export type X = Reverse<[
    //50
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

    //100
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

    //150
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

    //200
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

    //250
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

    //287
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    1, 2, 3, 4, 5, 6, 7
]>;
> tsc

Files:                        34
Lines:                      7450
Nodes:                     21256
Identifiers:                6781
Symbols:                  214344
Types:                     92308
Memory used:              85343K
Assignability cache size:   2014
Identity cache size:           0
Subtype cache size:            0
I/O Read time:             0.00s
Parse time:                0.12s
Program time:              0.13s
Bind time:                 0.05s
Check time:                0.81s
transformTime time:        0.02s
Source Map time:           0.01s
commentTime time:          0.01s
I/O Write time:            0.00s
printTime time:            0.04s
Emit time:                 0.04s
Total time:                1.02s

image


And another test with,

import {Concat} from "./concat";

export type Y = Concat<
    [
        //50
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //100
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //150
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //200
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //250
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //287
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7
    ],
    [
        //50
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //100
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //150
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //200
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //250
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

        //287
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        1, 2, 3, 4, 5, 6, 7
    ]
>;

declare function foo () : Y;
export const x = foo();
> tsc

Files:                         35
Lines:                       7542
Nodes:                      21877
Identifiers:                 6794
Symbols:                   485364
Types:                     220970
Memory used:              156981K
Assignability cache size:    3546
Identity cache size:            0
Subtype cache size:             0
I/O Read time:              0.01s
Parse time:                 0.14s
Program time:               0.16s
Bind time:                  0.06s
Check time:                 1.91s
transformTime time:         0.03s
Source Map time:            0.01s
commentTime time:           0.00s
I/O Write time:             0.00s
printTime time:             0.06s
Emit time:                  0.06s
Total time:                 2.19s

And the output,

import { Concat } from "./concat";
export declare type Y = Concat<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7]>;
export declare const x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7];
//# sourceMappingURL=index.d.ts.map

image


Not bad, considering I just concated two tuples, length 287 each, to become one tuple of length 574 during compile-time. (I don't actually do this in my application code, though).

The most I've done in my projects is concated a tuple of length 50+, I think.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 16, 2019

https://github.com/AnyhowStep/ts-trampoline-test

I just realized looking at Memory used isn't too helpful for such small examples. When I put the Reverse and Concat tests together, memory usage was 153478K, for a single run.

Memory usage for Concat alone was 156981K, for a single run. (Notice it is higher than Concat + Reverse tests combined)

Reverse alone was 85343K, for a single run.

@pirix-gh how are you benchmarking performance and memory usage?

And what's your "largest" test? I know you have a hard limit of 50 for each recursive type, but is there a particular test where you compose types until they just become a massive computational nightmare?

@jcalz
Copy link
Contributor

jcalz commented Jan 11, 2020

I am reminded:

It's too late to decide that we're not ready for recursive conditional types; they're here already. Now let's make them safe and easy to use! Maybe some kind of Defer<T, D, N> where T is some arbitrarily self-referencing type, D is some default type in case T can't be resolved by recursing at most N levels deep, and you can leave off N and get the maximum value allowed by the compiler, which is something big.

Otherwise I either have to keep giving answers like this one and/or impersonating an officer of the law.

@jcalz
Copy link
Contributor

jcalz commented May 1, 2020

Hmm, look at that! Recursive flat()! (#32131) Not sure what to make of it.

EDIT: #38298

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Aug 11, 2020
@jcalz
Copy link
Contributor

jcalz commented Aug 16, 2020

Oh wow #40002 🌟 🎉 🎆 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants