Skip to content

[API Question] Parentheses affects declaration emit. Intentional? #32824

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 Aug 12, 2019 · 5 comments · Fixed by #32924
Closed

[API Question] Parentheses affects declaration emit. Intentional? #32824

AnyhowStep opened this issue Aug 12, 2019 · 5 comments · Fixed by #32924
Assignees
Labels
Bug A bug in TypeScript

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 12, 2019

Consider the following snippet,

/*
    The below types are exactly the same.
    The only difference is that one is wrapped in parentheses.
    The other is not.
*/

type InvalidKeys<K extends string|number|symbol> = { [P in K]? : never };
type InvalidKeys2<K extends string|number|symbol> = (
    { [P in K]? : never }
);

type A<T> = (
    T & InvalidKeys<"a">
);
type A2<T> = (
    T & InvalidKeys2<"a">
);

/*
    type a = {
        x: number;
    } & InvalidKeys<"a">
 */
type a = A<{ x : number }>;
/*
    type a2 = {
        x: number;
    } & {
        a?: undefined;
    }
*/
type a2 = A2<{ x : number }>;

/*
    One parentheses is all you need to change the output
*/

Playground

I've noticed that, sometimes, the presence/absence of parentheses can change the emitted type declaration. Is this intentional? If so, are there set rules for it? Can this behaviour be relied upon?

There are times where one would want the type to be expanded. And other times where one would not want the type to be expanded.

Having control over it would be nice. (I know I've wanted control over it plenty of times).


I have encountered this discrepancy multiple times but this is the most recent example,
(that inspired me to finally make this issue).
#20863 (comment)

The InvalidKeys type is not expanded because I did not wrap the type in parentheses.


Obligatory @keithlayne , who hates unnecessary parentheses 🚎

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Aug 14, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Aug 14, 2019
@weswigham
Copy link
Member

I don't think this is intentional - we normally skip past parens in anywhere where we traverse structure, I'm guessing that when we assign an alias symbol for the mapped type, we're neglecting to do so.

It's worth noting though, that even if we patch this, the following:

type InvalidKeys3<K extends string|number|symbol> = {
    "0": { [P in K]? : never }
}["0"];

Is probably still going to work as a way to avoid giving the mapped type an alias without changing its meaning.

@AnyhowStep
Copy link
Contributor Author

Ooooooo

I just learned a new trick!

@weswigham
Copy link
Member

weswigham commented Aug 14, 2019

If you put anything other than a constant in that index position, you best name that object you're indexing into, y'hear? I am not responsible for how ugly your printback types get once you start immediately switch-indexing on anonymous objects.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 15, 2019

@weswigham @RyanCavanaugh

Could priority for fixing this "bug" be bumped up?
And maybe also merged into 3.5.1 after it's fixed?

I just realized that this parentheses-causing-expansion thing can actually cause really bad .d.ts output.

Bad enough that the project will emit .d.ts files fine...
But other projects using the .d.ts files will have a stack overflow!


/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:37580
        function getLateBoundSymbol(symbol) {
                                   ^
RangeError: Maximum call stack size exceeded
    at getLateBoundSymbol (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:37580:36)
    at getSymbolOfNode (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:33831:51)
    at appendTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36781:99)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36819:57)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)
    at getOuterTypeParameters (/home/anyhowstep/node-projects/tsql/tsql/node_modules/typescript/lib/typescript.js:36812:51)

I almost always use parentheses for multi-line types,

export type IntersectUsedRef<
    U extends AnyRawExpr
> = (
    UsedRef<U> extends never ?
    IUsedRef<{}> :
    UsedRefUtil.Intersect<
        UsedRef<U>
    >
);

The above actually causes the output to be super long!
image

And if I try to use this emitted .d.ts file, I get the stack overflow problem!

It appears it keeps trying to resolve the same file over and over and over again,

/home/anyhowstep/node-projects/tsql/tsql/dist/index.d.ts
/home/anyhowstep/node-projects/tsql/tsql/dist/index.d.ts
node.argument.literal TokenObject {
  pos: 36129,
  end: 36136,
  flags: 4194304,
  modifierFlagsCache: 0,
  transformFlags: 0,
  parent:
   NodeObject {
     pos: 36129,
     end: 36136,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent:
      NodeObject {
        pos: 36121,
        end: 36402,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [NodeObject],
        kind: 184,
        argument: [Circular],
        qualifier: [IdentifierObject],
        typeArguments: [Array],
        id: 37465 },
     kind: 183,
     literal: [Circular] },
  kind: 10,
  text: '../..' } NodeObject {
  pos: 36121,
  end: 36402,
  flags: 4194304,
  modifierFlagsCache: 0,
  transformFlags: 0,
  parent:
   NodeObject {
     pos: 36054,
     end: 38678,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent:
      NodeObject {
        pos: 36053,
        end: 38679,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [NodeObject],
        kind: 178,
        type: [Circular] },
     kind: 176,
     checkType:
      NodeObject {
        pos: 36054,
        end: 36059,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 165,
        typeName: [IdentifierObject],
        id: 37469 },
     extendsType:
      NodeObject {
        pos: 36067,
        end: 36119,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 184,
        argument: [NodeObject],
        qualifier: [IdentifierObject],
        typeArguments: [Array],
        id: 37461 },
     trueType: [Circular],
     falseType:
      NodeObject {
        pos: 36404,
        end: 38678,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 176,
        checkType: [NodeObject],
        extendsType: [NodeObject],
        trueType: [NodeObject],
        falseType: [TokenObject] } },
  kind: 184,
  argument:
   NodeObject {
     pos: 36129,
     end: 36136,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent: [Circular],
     kind: 183,
     literal:
      TokenObject {
        pos: 36129,
        end: 36136,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 10,
        text: '../..' } },
  qualifier:
   IdentifierObject {
     pos: 36138,
     end: 36146,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent: [Circular],
     escapedText: 'IUsedRef',
     flowNode: { flags: 2 },
     id: 37466 },
  typeArguments:
   [ NodeObject {
       pos: 36147,
       end: 36401,
       flags: 4194304,
       modifierFlagsCache: 0,
       transformFlags: 0,
       parent: [Circular],
       kind: 176,
       checkType: [NodeObject],
       extendsType: [NodeObject],
       trueType: [NodeObject],
       falseType: [TokenObject],
       id: 37467 },
     pos: 36147,
     end: 36401 ],
  id: 37465 }
/home/anyhowstep/node-projects/tsql/tsql/dist/index.d.ts
/home/anyhowstep/node-projects/tsql/tsql/dist/index.d.ts
node.argument.literal TokenObject {
  pos: 36129,
  end: 36136,
  flags: 4194304,
  modifierFlagsCache: 0,
  transformFlags: 0,
  parent:
   NodeObject {
     pos: 36129,
     end: 36136,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent:
      NodeObject {
        pos: 36121,
        end: 36402,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [NodeObject],
        kind: 184,
        argument: [Circular],
        qualifier: [IdentifierObject],
        typeArguments: [Array],
        id: 37465 },
     kind: 183,
     literal: [Circular] },
  kind: 10,
  text: '../..' } NodeObject {
  pos: 36121,
  end: 36402,
  flags: 4194304,
  modifierFlagsCache: 0,
  transformFlags: 0,
  parent:
   NodeObject {
     pos: 36054,
     end: 38678,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent:
      NodeObject {
        pos: 36053,
        end: 38679,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [NodeObject],
        kind: 178,
        type: [Circular] },
     kind: 176,
     checkType:
      NodeObject {
        pos: 36054,
        end: 36059,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 165,
        typeName: [IdentifierObject],
        id: 37469 },
     extendsType:
      NodeObject {
        pos: 36067,
        end: 36119,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 184,
        argument: [NodeObject],
        qualifier: [IdentifierObject],
        typeArguments: [Array],
        id: 37461 },
     trueType: [Circular],
     falseType:
      NodeObject {
        pos: 36404,
        end: 38678,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 176,
        checkType: [NodeObject],
        extendsType: [NodeObject],
        trueType: [NodeObject],
        falseType: [TokenObject] } },
  kind: 184,
  argument:
   NodeObject {
     pos: 36129,
     end: 36136,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent: [Circular],
     kind: 183,
     literal:
      TokenObject {
        pos: 36129,
        end: 36136,
        flags: 4194304,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular],
        kind: 10,
        text: '../..' } },
  qualifier:
   IdentifierObject {
     pos: 36138,
     end: 36146,
     flags: 4194304,
     modifierFlagsCache: 0,
     transformFlags: 0,
     parent: [Circular],
     escapedText: 'IUsedRef',
     flowNode: { flags: 2 },
     id: 37466 },
  typeArguments:
   [ NodeObject {
       pos: 36147,
       end: 36401,
       flags: 4194304,
       modifierFlagsCache: 0,
       transformFlags: 0,
       parent: [Circular],
       kind: 176,
       checkType: [NodeObject],
       extendsType: [NodeObject],
       trueType: [NodeObject],
       falseType: [TokenObject],
       id: 37467 },
     pos: 36147,
     end: 36401 ],
  id: 37465 }

^ Multiply the above output by, like, a billion


However, if I change it to,

//Notice the lack of parentheses
export type IntersectUsedRef<
    U extends AnyRawExpr
> = 
    UsedRef<U> extends never ?
    IUsedRef<{}> :
    UsedRefUtil.Intersect<
        UsedRef<U>
    >
;

I get a MUCH shorter output,
image

Notice how both output a "mere" 225 lines.
But the difference in the length of the 224th line is huuuuuge


I thought it was cool that parentheses would cause output to be expanded but I regret that now, lol.
It's terrible because I always uses parentheses and almost never intend for it to be expanded!

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 15, 2019

Also, I think this parentheses thing might partially be the cause of my other emit problems like in,
#31824

Or maybe even,
microsoft/TypeScript-Website#38

Because, well, I always use parentheses for complex type aliases.


@keithlayne Look! A world where unnecessary parentheses crashes your code!
Your dream world! 🚎

I didn't even need 400+ parentheses this time!
All it took was just one.


The compiler when I add one parentheses,
image

I am One Parentheses Man.

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

Successfully merging a pull request may close this issue.

3 participants