Skip to content

fix54035, extractType now allows parts of union and intersection types to be extracted #56131

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

Merged
merged 8 commits into from
Oct 28, 2023

Conversation

iisaduan
Copy link
Member

Fixes #54035

@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Oct 16, 2023
Comment on lines 318 to 338
if (isArray(selection)) {
const newTypeValue = isUnionTypeNode(selection[0].parent) ? factory.createUnionTypeNode(selection) : factory.createIntersectionTypeNode(selection);
const newTypeDeclaration = factory.createTypeAliasDeclaration(
/*modifiers*/ undefined,
name,
typeParameters.map(id => factory.updateTypeParameterDeclaration(id, id.modifiers, id.name, id.constraint, /*defaultType*/ undefined)),
newTypeValue,
);
changes.insertNodeBefore(file, enclosingNode, ignoreSourceNewlines(newTypeDeclaration), /*blankLineBetween*/ true);
changes.replaceNodeRange(file, selection[0], selection[selection.length - 1], factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /*typeArguments*/ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
}
else {
const newTypeNode = factory.createTypeAliasDeclaration(
/*modifiers*/ undefined,
name,
typeParameters.map(id => factory.updateTypeParameterDeclaration(id, id.modifiers, id.name, id.constraint, /*defaultType*/ undefined)),
selection,
);
changes.insertNodeBefore(file, enclosingNode, ignoreSourceNewlines(newTypeNode), /*blankLineBetween*/ true);
changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /*typeArguments*/ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it feels like like a lot of this can probably be deduplicated between the array and non-array cases. Maybe instead of an if/else the newTypeValue/newTypeNode variable could incorporate an isArray(selection) into its assignment. Then, you should be able to use changes.replaceNodeRange in both cases (instead of changes.replaceNode in the else, specifying selection as both the start and end of the range)

@gabritto
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 16, 2023

Heya @gabritto, I've started to run the tarball bundle task on this PR at 25214a5. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 16, 2023

Hey @gabritto, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/158246/artifacts?artifactName=tgz&fileId=757806A4DE6E41A4AD666062F7A45B9502DC0D19C5AC00E33777F87C281E3A6E02&fileName=/typescript-5.3.0-insiders.20231016.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.3.0-pr-56131-2".;

@gabritto
Copy link
Member

What happens if the selection doesn't fully enclose the types in a union?
I tried out these cases:

type a = { a: string /*1*/} | { b: string }/*2*/ | { c: string };

and it becomes this:

type NewType = {
  a: string;
};

type a = NewType | { b: string } | { c: string };

and

type a = { a: st/*1*/ring } | { b: string }/*2*/ | { c: string };

which becomes this:

type NewType = string;

type a = { a: NewType } | { b: string } | { c: string };

Those surprised me a bit, but tbh I don't know how much we care about this or how much people are annoyed or not by this behavior.

@andrewbranch
Copy link
Member

When @navya9singh implemented partial selection ranges for move to file, I think the logic was basically that the selection shrinks over whitespace and otherwise grows to encompass a valid set of nodes to move. It would be nice to have similar behavior between all selection-based refactors. So for example

type a = { a: string }/*1*/ | { b: string } | /*2*/{ c: string }; // extract b
type a = { a: string /*1*/} | { b: string }/*2*/ | { c: string }; // extract a and b
type a = { a: st/*1*/ring } | { b: string }/*2*/ | { c: string }; // extract a and b
type a = { a: st/*1*/ring }/*2*/ | { b: string } | { c: string }; // extract a
type a = { a: st/*1*/ring /*2*/} | { b: string } | { c: string }; // extract string

is probably what I’d expect

@iisaduan
Copy link
Member Author

iisaduan commented Oct 17, 2023

The original behavior didn't check for a lot of these imperfect selection cases, and as a result there is some weird behavior. For example

type A = /*1*/{ a: string } & { b: string } & { c: string }/*2*/ ;  // extracts a, b, c
type A = {/*1*/ a: string } & { b: string } & { c: string }/*2*/ ;  // extracts a
type A = { a: string } /*1*/& { b: string } & { c: string } ;/*2*/  // extracts a, b, c
type A = { a: st/*1*/ring }/*2*.; // extracts string

are all things that happen with the original behavior.

Those surprised me a bit, but tbh I don't know how much we care about this or how much people are annoyed or not by this behavior.

It would be nice to have similar behavior between all selection-based refactors.

I as well was surprised by this behavior, but I was also surprised that this partial type extraction bug wasn't noticed sooner. I think it would be really nice to have the behavior do be less finicky for the user and be more consistent across refactors, but again, not sure how much it's been noticed.

@gabritto
Copy link
Member

We might want to check with Matt, since he opened the original issue, to get a sense of how much this partial selection behavior matters.

@iisaduan
Copy link
Member Author

iisaduan commented Oct 18, 2023

After discussion with Matt today, and taking another look at the code that picks the nodes from the selection, adding small improvements such as

// type A = { a: str/*1*/ing } | { b: string } | { c: string }/*2*/ ;  // extracts string

becoming

// type A = { a: str/*1*/ing } | { b: string } | { c: string }/*2*/ ;  // extracts a, b, c

will require adjustments to the way I did the bug fix here. The existing code assumes the start of the selection is well placed, and my fix uses that assumption.

@iisaduan iisaduan requested a review from navya9singh October 23, 2023 23:36
@iisaduan
Copy link
Member Author

The most recent change only addresses the behavior discussed above, and doesn't require such a big change like I had originally expected.

Copy link
Member

@gabritto gabritto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left one small question, but otherwise looks good to me

const selectionRange = isArray(selection) ? { pos: selection[0].pos, end: selection[selection.length - 1].end } : selection;
if (isArray(selection)) {
selection.forEach(t => {
if (!visitor(t)) return undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what's going on here: if visitor returns true, that means we want to skip collection of type parameters and so we want collectTypeParameters to return undefined, right?
It seems to me we're not doing that anymore here when selection is an array, since inside this if we're always returning the result array.

@iisaduan iisaduan merged commit 6061069 into microsoft:main Oct 28, 2023
@iisaduan iisaduan deleted the extract-entire-alias branch March 27, 2024 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Extract type alias doesn't extract entire selected type
4 participants