Skip to content

some types compile to a massive conditional type that causes performance issues #51106

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
DetachHead opened this issue Oct 8, 2022 · 1 comment · Fixed by #51152
Closed
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.

Comments

@DetachHead
Copy link
Contributor

Bug Report

sorry i tried to create a minimal repro but this is the best i could do, so to reproduce this, run npm install @detachhead/ts-helpers

🔎 Search Terms

d.ts performance

🕗 Version & Regression Information

4.9.0-dev.20221007

💻 Code

takes like 2 minutes to compile:

import { power } from '@detachhead/ts-helpers/dist/utilityFunctions/Number'
power

takes a couple seconds to compile:

import { power } from '@detachhead/ts-helpers/src/utilityFunctions/Number'
power

🙁 Actual behavior

the power function is being compiled to return this huge conditional type that causes the compiler to hang for several minutes when it's imported:

// Number.ts
export const power = <Num extends number, PowerOf extends number>(
    num: Num,
    powerOf: PowerOf,
): Power<Num, PowerOf> => (num ** powerOf) as never
// Number.d.ts
export declare const power: <Num extends number, PowerOf extends number>(num: Num, powerOf: PowerOf) => number extends PowerOf ? PowerOf & number : PowerOf extends 0 ? 1 : number extends Subtract<PowerOf, 1> ? Subtract<PowerOf, 1> & number : Subtract<PowerOf, 1> extends infer T ? T extends Subtract<PowerOf, 1> ? T extends 0 ? Multiply<1, Num> : number extends Subtract<T, 1> ? Subtract<T, 1> & number : Subtract<T, 1> extends infer T_1 ? T_1 extends Subtract<T, 1> ? T_1 extends 0 ? Multiply<Multiply<1, Num>, Num> : number extends Subtract<T_1, 1> ? Subtract<T_1, 1> & number : Subtract<T_1, 1> extends infer T_2 ? T_2 extends Subtract<T_1, 1> ? T_2 extends 0 ? Multiply<Multiply<Multiply<1, Num>, Num>, Num> : number extends Subtract<T_2, 1> ? Subtract<T_2, 1> & number : Subtract<T_2, 1> extends infer T_3 ? T_3 extends Subtract<T_2, 1> ? T_3 extends 0 ? Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num> : number extends Subtract<T_3, 1> ? Subtract<T_3, 1> & number : Subtract<T_3, 1> extends infer T_4 ? T_4 extends Subtract<T_3, 1> ? T_4 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num> : number extends Subtract<T_4, 1> ? Subtract<T_4, 1> & number : Subtract<T_4, 1> extends infer T_5 ? T_5 extends Subtract<T_4, 1> ? T_5 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num>, Num> : number extends Subtract<T_5, 1> ? Subtract<T_5, 1> & number : Subtract<T_5, 1> extends infer T_6 ? T_6 extends Subtract<T_5, 1> ? T_6 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num>, Num>, Num> : number extends Subtract<T_6, 1> ? Subtract<T_6, 1> & number : Subtract<T_6, 1> extends infer T_7 ? T_7 extends Subtract<T_6, 1> ? T_7 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num> : number extends Subtract<T_7, 1> ? Subtract<T_7, 1> & number : Subtract<T_7, 1> extends infer T_8 ? T_8 extends Subtract<T_7, 1> ? T_8 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num> : number extends Subtract<T_8, 1> ? Subtract<T_8, 1> & number : Subtract<T_8, 1> extends infer T_9 ? T_9 extends Subtract<T_8, 1> ? T_9 extends 0 ? Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<Multiply<1, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num>, Num> : any : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never : never;

🙂 Expected behavior

// Number.d.ts
export declare const power: <Num extends number, PowerOf extends number>(
    num: Num,
    powerOf: PowerOf,
) => Power<Num, PowerOf>
@Andarist
Copy link
Contributor

Andarist commented Oct 8, 2022

I think that I was able to - sort of - reduce this. I've tried to provide a repro using the TS playground but this required multiple files and I couldn't make mult-file playground to work correctly despite the fact this should be possible and I think I've prepared it in the same way as described in the "docs".

TS playground

type PowerTailRec<PowerOf extends number> = number extends PowerOf ? number : 0;

type Power<PowerOf extends number> = PowerTailRec<PowerOf>;

// @filename: a.tsx
import { Power } from "./input";

export const power = <Num extends number, PowerOf extends number>(
  num: Num,
  powerOf: PowerOf
): Power<PowerOf> => (num ** powerOf) as never;

.d.ts output in the real project:

import { Power } from '../utilityTypes/Number';

export declare const power: <Num extends number, PowerOf extends number>(num: Num, powerOf: PowerOf) => number extends PowerOf ? PowerOf & number : 0;

Note that if I move the types to the file that contains this function declaration then the output changes to:

declare type PowerTailRec<PowerOf extends number> = number extends PowerOf ? number : 0;
declare type Power<PowerOf extends number> = PowerTailRec<PowerOf>;
export declare const power: <Num extends number, PowerOf extends number>(num: Num, powerOf: PowerOf) => PowerTailRec<PowerOf>;
export {};

We can notice some things here:

  • in both cases the Power gets actually inlined in the return type of power, and Power stays in the file even though it's unused (in the first case we have unused import, in the second case we have unused type alias declaration)
  • in the first case the inlining is recursive, whereas in the second case only "one level" gets inlined
  • with recursive inlining we can also see PowerOf & number in the true branch of the inlined conditional type. Perhaps this won't introduce any issues in this example but that intersection feels redundant and certainly looks different from the original conditional type that this corresponds to

If we change the declaration for Power to this:

export type Power<PowerOf extends number> = [PowerTailRec<PowerOf>]

then the generated declaration for the power doesn't inline the content of Power:

export declare const power: <Num extends number, PowerOf extends number>(num: Num, powerOf: PowerOf) => Power<PowerOf>;

All in all - it feels like this is related somehow to tail recursive conditional types, with which the inlining is too eager. IIRC @weswigham was working on a several PRs related to declaration emit recently - so I would expect the regression to be caused by one of those.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Oct 11, 2022
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.9.2 milestone Oct 11, 2022
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Oct 12, 2022
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 Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants