Аналог Union Types(TypeScript) в C#?

Как в C# объявить тип подобный Union Types из TypeScript?

type OnlyOne = "one" | "two" | "three"

function foo(value: OnlyOne) {
  // ...
}

foo("two") // Ok
foo("error") // Error => Argument of type '"error"' is not assignable to parameter of type 'OnlyOne'

Playground

Базовый тип предполагается один (например string или int), но значения, которые он может содержать, должны быть константными.

Т.е. что-то вроде такого:

void fun((string: "foo"|"bar") str) {}
fun("bar") // => Ok
fun("some") // => Error

Ответы (3 шт):

Автор решения: Alexander Brattsev

Вы можете создать новый тип, который будет содержать проверку, при преобразовании из старого, а преобразовывать с помощью implicit. Пример:

public class OneTwoString{
    public string Value{get;}

    public OneTwoString(string value){
    //валидация строки, типа:
    if (value == "One" || value == "Two")
    {
          Value = value;
    }else{
        throw new ArgumentOutOfRangeException();
    }
    }
    public static implicit operator OneTwoString(string value) => new OneTwoString(value);
}

public clsas Program{
    public void Main(){
      var value = "One";
      SomeMethod("One"); // отработает
      value = "Ten";
      SomeMethod(value); // ArgumentOutOfRangeException
    }

    public void SomeMethod(OneTwoString value){
      // value.Value;
    }
}
→ Ссылка
Автор решения: Blackmeser
class FUN
{
    public static string[] filter;
    public static void SetFilter(params string[] filter) => FUN.filter = filter;
    public void Fun(string str)
    {
        if (!(FUN.filter?.Contains(str) ?? true))
            throw new ArgumentOutOfRangeException();
        ///
        Console.WriteLine(str);
    }
}
FUN.SetFilter("foo", "bar");
new FUN().Fun("foo"); //foo
new FUN().Fun("some"); //System.ArgumentOutOfRangeException

Можно улучшить, передавая в FUN() ссылку на функцию, которую нужно выполнить после .Fun("foo"), которая будет выполняться вместо Console.WriteLine

Так-же можно отказаться от статического поля и будет выглядеть примерно так:
FUN fun1 = new FUN(Action<> action, params string[] filter)
А вызывать через:
fun1.Call(string str);
Тогда при FUN() вы создаёте объект с вызываемой функцией и фильтром, а при Call() фильтруете и если всё ок - выполняете функцию.

Пример кода:

//описание класса, выполняющего Action
class FUN
{
    string[] filter;
    Action action;
    public FUN(Action action, params string[] filter)
    {
        this.action = action;
        this.filter = filter;
    }
    public void Call(string str)
    {
        if (!(filter.Contains(str)) && filter.Length != 0)
            throw new ArgumentOutOfRangeException();
        ///
        action.Invoke();
    }
}
//функция, которая должна выполняться
void myMethod(object a)
{
    Console.WriteLine(a);
}
//упаковка Action (можно переделать и упростить)
Action myAction = () => myMethod(123);
//создание вызываемого экземпляра с функцией и фильтром
FUN fun1 = new FUN(myAction, "foo", "bar");
//вызов с фильтрацией
fun1.Call("foo"); //123
//вызов с фильтрацией
fun1.Call("some"); //System.ArgumentOutOfRangeException

Есть ещё вариант с цепочкой вызываемых функций/методов, суть в том что в первом вы вызываете проверку, а в последующих выполняется код:

//описываете делегат (контейнер функций/методов)
delegate void FilterCall(string str);
//создаёте конкретный экземпляр
FilterCall filtercall;
//описываете функцию проверки
void filtermethod(string str)
{
    if (!(new string[] { "foo","bar" }.Contains(str)))
        throw new ArgumentOutOfRangeException();
}
//добавляете функцию проверки
filtercall += filtermethod;
//описываете полезную функцию
void myMethod(string str)
{
    Console.WriteLine("Вызов myMethod с параметром: " + str);
}
//добавляете полезную функцию
filtercall += myMethod;
filtercall("foo"); //Вызов myMethod с параметром: foo
filtercall("some"); //System.ArgumentOutOfRangeException

Для разных типов:

class FUN<T>
{
    T[] filter;
    Action action;
    public FUN(Action action, params T[] filter)
    {
        this.action = action;
        this.filter = filter;
    }
    public void Call(T str)
    {
        if (!(filter.Contains(str)) && filter.Length != 0)
            throw new ArgumentOutOfRangeException();
        ///
        action.Invoke();
    }
}
FUN<int> fun1 = new FUN<int>(myAction, 1, 2);
fun1.Call(1); //123 т.к. в myMethod выше так и записали, можно принимать конкретное значение
fun1.Call(3); //System.ArgumentOutOfRangeException

Проверка на тип:

class FUN
{
    Type[] filter;
    Action action;
    public FUN(Action action, params Type[] filter)
    {
        this.action = action;
        this.filter = filter;
    }
    public void Call<T>(T data)
    {
        if (!(filter.Contains(data.GetType())) && filter.Length != 0)
            throw new ArgumentOutOfRangeException();
        ///
        action.Invoke();
    }
}
FUN fun1 = new FUN(myAction, typeof(int), typeof(string));
fun1.Call("123"); //123
fun1.Call(123); //123
fun1.Call(123.0); //System.ArgumentOutOfRangeException т.к. тип double
struct MyStruct { }
MyStruct ms = new MyStruct();
fun1.Call(ms); //System.ArgumentOutOfRangeException т.к. тип опять неподходящий
DateTime now = DateTime.Now;
fun1.Call(now); //System.ArgumentOutOfRangeException и время тоже не принимает
fun1.Call(int.Parse("123")); //123
→ Ссылка
Автор решения: Alexander Lonberg

Подобные вопросы, как оказалось, рассматриваются несколько лет.

Ссылки по теме:

Цитата из последнего комментария:

"...они значительно упростят представление данных без чрезмерного злоупотребления наследованием."(c)

... как раз чрезмерное наследование, мне и показалось излишним.

Самое интересное здесь: OneOf.

В примере ниже, метод принимает разнородные типы, не имеющие общего интерфейса, а OneOf возвращает обобщенный тип.

// Пихаем все в один НЕперегруженный метод, в точности как на TypeScript.
PrintAsHex((50, 150, 250));
PrintAsHex(EnumColor.Red);
PrintAsHex(new ClassColor());

// Без перегрузки выглядит намного лучше.
void PrintAsHex(MultiTypeColor mtc) {
  // а здесь мы уже работаем с MultiTypeColor
}

Нерабочий сниппет:

using System;
using System.Drawing;
using OneOf;

namespace DiscriminatedUnions {
  class Program {
    static void Main() {
      // Ну разве это не здорово
      Some.PrintAsHex((50, 150, 250));
      Some.PrintAsHex(EnumColor.Red);
      Some.PrintAsHex(new ClassColor());
      // Some.PrintAsRGB(new Some()); // А тут неизвестный тип => Error CS1503
    }
  }

  class Some {
    // В теле метода имеем нужный нам тип
    public static void PrintAsHex(MultiTypeColor mtc) {
      Console.WriteLine(mtc.Hex);
    }
  }

  enum EnumColor {
    Red,
    Blue
  }

  class ClassColor {
    public byte R = 10;
    public byte G = 20;
    public byte B = 30;
  }

  // Если не обращать внимания на это безобразие, выше все выглядит достаточно прилично.
  class MultiTypeColor : OneOfBase<(byte, byte, byte), EnumColor, ClassColor> {
    Func<string, string> get = v => Enum.IsDefined(typeof(KnownColor), v) ? v : "White";
    public Color Color { get; private set; }
    public string Hex { get => $"#{Color.R:X2}{Color.G:X2}{Color.B:X2}"; }
    MultiTypeColor(OneOf<(byte, byte, byte), EnumColor, ClassColor> _) : base(_) {
      Color = _.Index switch {
        0 => Color.FromArgb(_.AsT0.Item1, _.AsT0.Item2, _.AsT0.Item3),
        1 => Color.FromName(get(_.AsT1.ToString())),
        2 => Color.FromArgb(_.AsT2.R, _.AsT2.G, _.AsT2.B),
        _ => Color.FromName("White")
      };
    }
    public static implicit operator MultiTypeColor((byte, byte, byte) _) => new(_);
    public static implicit operator MultiTypeColor(EnumColor _) => new(_);
    public static implicit operator MultiTypeColor(ClassColor _) => new(_);
  }
}

... а здесь рабочий dotnetfiddle.net

Типизацию мы не нарушаем:

  • При вызове метода уже известны возможные типы.
  • В теле имеем тип параметра. Очень печалит отсутствие implicit operator в Typescript.

Я предполагал, что подобное встроено в язык, но видимо придется немного подождать.

→ Ссылка