RefactorMCP catalog

Convert Tuple to Named Type

Replaces a tuple in a method's signature, its return type or one of its parameters, with a named positional record, and updates the method's body and its callers to construct and read it.

Arguments

ArgumentMeaning
namethe name of the new type
parameterthe parameter whose tuple type is replaced; the return type when omitted
kindstruct for a readonly record struct (the default), class for a sealed record

The target is the method, by symbol.

Precondition

  • The return type, or the named parameter's type, is a tuple, and every element has a name.
  • No type named name is already visible where the type would be declared.
  • The project's language version supports the kind: C# 10 for a record struct, C# 9 for a record class.
  • Every use of the tuple that the refactoring does not rewrite still compiles with the named type. Values that flow on as tuples, such as a result stored in a variable declared with the tuple type, are refused.

Transformation

  • A positional record named name is declared after the type containing the method, with that type's accessibility. Its properties are the element names in Pascal case, with the element types. Type parameters the elements use become the record's type parameters, in order of first use.
  • The tuple type in the signature becomes the named type.
  • Tuple literals the method returns, or that callers pass for the parameter, become new Name(...), dropping element names.
  • Element accesses become property accesses, min or Item1 becoming Min, on the method's result, on a var local initialized from it, and on the parameter in the body.
  • Deconstruction keeps working through the record's Deconstruct.

Preserved

  • The values passed and returned, element access, deconstruction and value equality.

Limitations

  • ToString prints the record's format, MinMax { Min = 1, Max = 2 }, rather than the tuple's (1, 2).
  • The properties are read-only; code that assigned an element no longer compiles and the change is refused.
  • With class, default of the type is null rather than a tuple of defaults.
  • Only the one signature changes; overrides and interface implementations that share it are refused by the compile check.

Error codes

CodeMeaning
not-a-tuplethe return type or parameter is not a tuple
unnamed-elementsa tuple element has no name
name-conflicta type named name is already visible there
language-versionthe project's language version predates the record kind
used-as-tuplea use of the tuple would not compile with the named type

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#generic-method

Makes the named type generic over the type parameter the tuple uses

success
Targetsymbol M:Shop.Lists.Ends``1(System.Collections.Generic.IReadOnlyList{``0})
Arguments
name"Bounds"
Lists.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop;
44
55 public static class Lists
66 {
7− public static (T head, T tail) Ends<T>(IReadOnlyList<T> items) => (items[0], items[items.Count - 1]);
7+ public static Bounds<T> Ends<T>(IReadOnlyList<T> items) => new Bounds<T>(items[0], items[items.Count - 1]);
88
9− public static string Outer(IReadOnlyList<string> names) => Ends(names).head + Ends(names).tail;
9+ public static string Outer(IReadOnlyList<string> names) => Ends(names).Head + Ends(names).Tail;
1010 }
11+
12+public readonly record struct Bounds<T>(T Head, T Tail);

#language-version-rejected

Refuses a record struct when the project's language version predates them

refusal
Targetsymbol M:Shop.Stats.Range(System.Int32[])
Arguments
name"MinMax"
ProjectlangVersion 9
Refuseslanguage-version; every file is left unchanged
Stats.csinput
1 namespace Shop
2 {
3 public static class Stats
4 {
5 public static (int min, int max) Range(int[] values) => (values[0], values[values.Length - 1]);
6 }
7 }

#name-conflict-rejected

Refuses a type name that is already taken

refusal
Targetsymbol M:Shop.Stats.Range(System.Int32[])
Arguments
name"Range"
Refusesname-conflict; every file is left unchanged
Stats.csinput
1 namespace Shop;
2
3 public class Range
4 {
5 }
6
7 public static class Stats
8 {
9 public static (int min, int max) Range(int[] values) => (values[0], values[values.Length - 1]);
10 }

#not-a-tuple-rejected

Refuses a method whose return type is not a tuple

refusal
Targetsymbol M:Shop.Stats.Count(System.Int32[])
Arguments
name"MinMax"
Refusesnot-a-tuple; every file is left unchanged
Stats.csinput
1 namespace Shop;
2
3 public static class Stats
4 {
5 public static int Count(int[] values) => values.Length;
6 }

#parameter-tuple

Replaces a tuple parameter, constructing the type from tuple literals at call sites and renaming element accesses in the body

success
Targetsymbol M:Shop.Printer.Print(System.ValueTuple{System.String,System.Int32})
Arguments
name"OrderLine"
parameter"line"
Printer.csmodified
11 namespace Shop;
22
33 internal class Printer
44 {
5− public string Print((string name, int quantity) line) => line.name + " x" + line.quantity;
5+ public string Print(OrderLine line) => line.Name + " x" + line.Quantity;
66
7− public string Sample() => Print(("tea", 2)) + Print(line: (name: "cake", quantity: 1));
7+ public string Sample() => Print(new OrderLine("tea", 2)) + Print(line: new OrderLine("cake", 1));
88 }
9+
10+internal readonly record struct OrderLine(string Name, int Quantity);

#record-class

Declares a sealed record class instead of a record struct when kind is class, keeping element names that are already Pascal case

success
Targetsymbol M:Shop.Menu.Cheapest
Arguments
name"Offer"
kind"class"
Menu.csmodified
11 namespace Shop
22 {
33 public class Menu
44 {
5− public (string Name, decimal Price) Cheapest() => ("tea", 1.5m);
5+ public Offer Cheapest() => new Offer("tea", 1.5m);
66
77 public string Label() => Cheapest().Name;
88 }
9+
10+ public sealed record Offer(string Name, decimal Price);
911 }

#return-tuple-with-callers-in-another-file

Replaces a method's tuple return type with a readonly record struct, constructing it in the return and renaming element accesses in callers; deconstruction keeps working

success
Targetsymbol M:Shop.Stats.Range(System.Int32[])
Arguments
name"MinMax"
Report.csmodified
11 namespace Shop;
22
33 public static class Report
44 {
55 public static string Describe(int[] values)
66 {
77 var range = Stats.Range(values);
8− return range.min + ".." + range.max;
8+ return range.Min + ".." + range.Max;
99 }
1010
1111 public static int Width(int[] values)
1212 {
1313 var (low, high) = Stats.Range(values);
1414 return high - low;
1515 }
1616
17− public static int Top(int[] values) => Stats.Range(values).Item2;
17+ public static int Top(int[] values) => Stats.Range(values).Max;
1818 }
Stats.csmodified
11 namespace Shop;
22
33 public static class Stats
44 {
5− public static (int min, int max) Range(int[] values)
5+ public static MinMax Range(int[] values)
66 {
77 var min = int.MaxValue;
88 var max = int.MinValue;
99 foreach (var value in values)
1010 {
1111 if (value < min) min = value;
1212 if (value > max) max = value;
1313 }
1414
1515 // Empty input gives an inverted range.
16− return (min, max);
16+ return new MinMax(min, max);
1717 }
1818 }
19+
20+public readonly record struct MinMax(int Min, int Max);

#unnamed-elements-rejected

Refuses a tuple whose elements have no names to give the properties

refusal
Targetsymbol M:Shop.Stats.Range(System.Int32[])
Arguments
name"MinMax"
Refusesunnamed-elements; every file is left unchanged
Stats.csinput
1 namespace Shop;
2
3 public static class Stats
4 {
5 public static (int, int) Range(int[] values) => (values[0], values[values.Length - 1]);
6 }

#used-as-tuple-rejected

Refuses when a caller stores the result in a variable declared with the tuple type

refusal
Targetsymbol M:Shop.Stats.Range(System.Int32[])
Arguments
name"MinMax"
Refusesused-as-tuple mentioning “Report.cs”; every file is left unchanged
Report.csinput
1 namespace Shop;
2
3 public static class Report
4 {
5 public static int Width(int[] values)
6 {
7 (int min, int max) range = Stats.Range(values);
8 return range.max - range.min;
9 }
10 }
Stats.csinput
1 namespace Shop;
2
3 public static class Stats
4 {
5 public static (int min, int max) Range(int[] values) => (values[0], values[values.Length - 1]);
6 }