Typescript: Расширение строковых перечислений

Созданный на 3 авг. 2017  ·  68Комментарии  ·  Источник: microsoft/TypeScript

До появления перечислений на основе строк многие возвращались к объектам. Использование объектов также позволяет расширять типы. Например:

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};

const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

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

Мне было бы очень полезно иметь возможность сделать что-то вроде этого:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Учитывая, что созданные перечисления являются объектами, это тоже не будет слишком ужасно:

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};
Awaiting More Feedback Suggestion

Самый полезный комментарий

Все обходные пути хороши, но я хотел бы видеть поддержку наследования перечисления от самого машинописного текста, чтобы я мог использовать исчерпывающие проверки как можно проще.

Все 68 Комментарий

Просто немного поигрался с ним, и в настоящее время это расширение можно сделать, используя объект для расширенного типа, так что это должно работать нормально:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

Обратите внимание, вы можете приблизиться с

enum E {}

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
function enumerate<T1 extends typeof E, T2 extends typeof E>(e1: T1, e2: T2) {
  enum Events {
    Restart = 'Restart'
  }
  return Events as typeof Events & T1 & T2;
}

const e = enumerate(BasicEvents, AdvEvents);

Другой вариант, в зависимости от ваших потребностей, — использовать тип объединения:

const enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
const enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;

let e: Events = AdvEvents.Pause;

Недостатком является то, что вы не можете использовать Events.Pause ; вы должны использовать AdvEvents.Pause . Если вы используете константные перечисления, это, вероятно, нормально. В противном случае этого может быть недостаточно для вашего варианта использования.

Нам нужна эта функция для строго типизированных редукторов Redux. Пожалуйста, добавьте его в TypeScript.

Другой обходной путь — не использовать перечисления, а использовать что-то похожее на перечисление:

const BasicEvents = {
  Start: 'Start' as 'Start',
  Finish: 'Finish' as 'Finish'
};
type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];

const AdvEvents = {
  ...BasicEvents,
  Pause: 'Pause' as 'Pause',
  Resume: 'Resume' as 'Resume'
};
type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];

Все обходные пути хороши, но я хотел бы видеть поддержку наследования перечисления от самого машинописного текста, чтобы я мог использовать исчерпывающие проверки как можно проще.

Просто используйте класс вместо перечисления.

Я просто пробовал это.

const BasicEvents = {
    Start: 'Start' as 'Start',
    Finish: 'Finish' as 'Finish'
};

type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];

const AdvEvents = {
    ...BasicEvents,
    Pause: 'Pause' as 'Pause',
    Resume: 'Resume' as 'Resume'
};

type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];

type sometype<T extends AdvEvents> =
    T extends typeof AdvEvents.Start ? 'Some String' :
    T extends typeof AdvEvents.Finish ? 'Some Other String' :
    T extends typeof AdvEvents.Pause ? 'Abc' :
    T extends typeof AdvEvents.Resume ? 'Xyz' : never;
type r = sometype<typeof AdvEvents.Finish>;

Должен быть лучший способ сделать это.

Почему это еще не функция? Никаких критических изменений, интуитивно понятное поведение, более 80 человек, которые активно искали и требовали эту функцию — кажется, это не проблема.

Даже повторный экспорт перечисления из другого файла в пространстве имен действительно странен без расширения перечислений (и невозможно повторно экспортировать перечисление таким образом, чтобы оно по-прежнему было перечислением , а не объектом и типом):

import { Foo as _Foo } from './Foo';

namespace Bar
{
    enum Foo extends _Foo {} // nope, doesn't work

    const Foo = _Foo;
    type Foo = _Foo;
}

Bar.Foo // actually not an enum

obrazek

+1
В настоящее время используется обходной путь, но это должна быть встроенная функция перечисления.

Я просмотрел этот выпуск, чтобы узнать, не задавал ли кто-нибудь следующий вопрос. (Кажется, нет.)

Из ОП:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Будут ли люди ожидать, что AdvEvents можно будет присвоить BasicEvents ? (Как, например, в случае с extends для классов.)

Если да, то насколько хорошо это согласуется с тем фактом, что типы enum должны быть окончательными и не могут быть расширены?

@masak отличный момент. Функция, которую люди хотят здесь, определенно не похожа на обычную extends . BasicEvents следует присваивать AdvEvents , а не наоборот. Обычный extends уточняет другой тип, делая его более конкретным, и в этом случае мы хотим расширить другой тип, чтобы добавить больше значений, поэтому любой пользовательский синтаксис для этого, вероятно, не должен использовать ключевое слово extends , или, по крайней мере, не используйте синтаксис enum A extends B { .

На этой ноте мне понравилось предложение распространения для этого от OP.

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

Потому что распространение уже несет в себе ожидание того, что оригинал будет поверхностно клонирован в несвязанную копию.

BasicEvents следует присваивать AdvEvents , а не наоборот.

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

Я еще немного подумал об обходных путях, и, отработав https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147, вы можете сделать немного лучше, также определив Events в значении пространство:

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

let e: Events = Events.Pause;

Из моего тестирования видно, что Events.Start правильно интерпретируется как BasicEvents.Start в системе типов, поэтому проверка полноты и уточнение размеченных объединений работают нормально. Главное, чего не хватает, это то, что вы не можете использовать Events.Pause в качестве литерала типа; вам нужно AdvEvents.Pause . Вы можете использовать typeof Events.Pause , и он разрешается в AdvEvents.Pause , хотя люди в моей команде были сбиты с толку таким шаблоном, и я думаю, что на практике я бы поощрял AdvEvents.Pause при использовании это как тип.

(Это для случая, когда вы хотите, чтобы типы перечислений могли назначаться между собой, а не между отдельными перечислениями. По моему опыту, чаще всего требуется, чтобы они были назначаемыми.)

Еще одно предложение (хотя оно и не решает исходной проблемы): как насчет использования строковых литералов для создания объединения типов?

type BEs = "Start" | "Finish";

type AEs = BEs | "Pause" | "Resume";

let example: AEs = "Finish"; // there is even autocompletion

Значит, решение наших проблем может быть таким?

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
// { Start: string, Finish: string };


const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
} as const
// { Start: 'Start', Finish: 'Finish' };

https://github.com/Microsoft/TypeScript/pull/29510

Расширение перечислений должно быть основной функцией TypeScript. Просто говорю'

@wottpal Повторяю мой предыдущий вопрос:

Если [перечисления могут быть расширены], то насколько хорошо это согласуется с тем фактом, что типы перечислений должны быть окончательными и не могут быть расширены?

В частности, мне кажется, что проверка тотальности оператора switch по значению перечисления зависит от нерасширяемости перечислений.

@масак Что? Нет, это не так! Поскольку расширенное перечисление является более широким типом и не может быть присвоено исходному перечислению, вы всегда знаете все значения каждого используемого вами перечисления. Расширение в этом контексте означает создание нового перечисления, а не изменение старого.

enum A { a; }
enum B extends A { b; }

declare var a: A;
switch(a) {
    case A.a:
        break;
    default:
        // a is never
}

declare var b: B;
switch(b) {
    case A.a:
        break;
    default:
        // b is B.b
}

@ m93a А, так вы имеете в виду, что extends здесь, по сути, имеет больше семантики _copying_ (значений перечисления из A в B )? Тогда да, переключатели выходят нормально.

Тем не менее, есть _некоторые_ ожидания, которые все еще кажутся мне несостоятельными. Чтобы попытаться зафиксировать это: с классами extends _не_ передает семантику копирования — поля и методы не копируются в расширяющийся подкласс; вместо этого они просто становятся доступными через цепочку прототипов. В суперклассе всегда есть только одно поле или метод.

Из-за этого, если class B extends A , мы гарантируем, что B можно присвоить A , и поэтому, например, let a: A = new B(); будет вполне нормально.

Но с перечислениями и extends мы бы не смогли сделать let a: A = B.b; , потому что нет такой соответствующей гарантии. Вот что мне кажется странным; extends здесь передает определенный набор предположений о том, что можно сделать, и они не встречаются с перечислениями.

Тогда просто назовите это expands или clones ? 🤷‍♂️
С точки зрения пользователей кажется странным, что чего-то столь простого достичь не просто.

Если разумная семантика требует совершенно нового ключевого слова (без большого количества предшествующего уровня техники на других языках), почему бы вместо этого повторно не использовать синтаксис распространения ( ... ), как это предлагается в OP и этом комментарии ?

+1 Я надеюсь, что это будет добавлено к функции перечисления по умолчанию. :)

Кто-нибудь знает какие-нибудь элегантные решения??? 🧐

Если разумная семантика требует совершенно нового ключевого слова (без большого количества предшествующего уровня техники на других языках), почему бы вместо этого повторно не использовать синтаксис распространения (...), как это предлагается в ОП и этом комментарии?

Да, немного подумав об этом, я думаю, что это решение было бы хорошим.

После прочтения всей этой ветки проблем, кажется, что существует широкое согласие в том, что повторное использование оператора распространения решает проблему и устраняет все опасения, которые люди высказывали по поводу запутанности/неинтуитивности синтаксиса.

// extend enum using spread
enum AdvancedEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

@RyanCavanaugh , действительно ли эта проблема все еще нуждается в пометке «Ожидание дополнительных отзывов» на данный момент?

Требуемая функция +1

Есть ли у нас новости по этому вопросу? Было бы очень полезно реализовать оператор расширения для перечислений.

Специально для случаев использования, связанных с метапрограммированием, возможность использовать псевдонимы и расширять перечисления находится где-то между необходимостью и желательностью. В настоящее время нет способа взять одно перечисление и export под другим именем, если только вы не прибегнете к одному из обходных путей, упомянутых выше.

@ m93a А, так вы имеете в виду, что extends здесь, по сути, имеет больше семантики _copying_ (значений перечисления из A в B )? Тогда да, переключатели выходят нормально.

Тем не менее, есть _некоторые_ ожидания, которые все еще кажутся мне несостоятельными. Чтобы попытаться зафиксировать это: с классами extends _не_ передает семантику копирования — поля и методы не копируются в расширяющийся подкласс; вместо этого они просто становятся доступными через цепочку прототипов. В суперклассе всегда есть только одно поле или метод.

Из-за этого, если class B extends A , мы гарантируем, что B можно присвоить A , и поэтому, например, let a: A = new B(); будет вполне нормально.

Но с перечислениями и extends мы бы не смогли сделать let a: A = B.b; , потому что нет такой соответствующей гарантии. Вот что мне кажется странным; extends здесь передает определенный набор предположений о том, что можно сделать, и они не встречаются с перечислениями.

@masak Я думаю, что ты близок к правильному, но ты сделал одно небольшое предположение, которое неверно. B присваивается A в случае enum B extends A , поскольку «присваиваемый» означает, что все значения, предоставленные A, доступны в B. Когда вы сказали, что let a: A = B.b , вы предполагаете, что значения в B должны быть доступны в A, что не совпадает со значениями, присваиваемыми A. let a: A = B.a IS правильно, потому что B присваивается A.

Это очевидно, используя классы, как в следующем примере:

class A {
 a() {}
}

class B extends A {
 b() {}
}

let a: A = new B();

a.a();  // valid
a.b();  // invalid via type system since `a` is typed as `A`

Ссылка на игровую площадку TypeScript

invalid access

Короче говоря, я считаю, что расширение IS является правильной терминологией, поскольку именно это и делается. В примере enum B extends A вы можете ВСЕГДА ожидать, что перечисление B будет содержать все возможные значения перечисления A, потому что B является «подклассом» (subenum? может быть, есть лучшее слово для этого) A и, таким образом, возлагается на А.

Поэтому я не думаю, что нам нужно новое ключевое слово, я думаю, что мы должны использовать расширения, И я думаю, что это должно быть изначально частью TypeScript: D

@julian-sf Думаю, я согласен со всем, что вы написали...

...но... :slightly_smiling_face:

как я проблематизировал здесь , как насчет этой ситуации?

// example from OP
enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

Учитывая, что Pause является _экземпляром_ AdvEvents и AdvEvents _extends_ BasicEvents , вы также ожидаете, что Pause будет _экземпляром_ BasicEvents ? (Потому что это, кажется, следует из того, как обычно взаимодействуют отношения экземпляр/наследие.)

С другой стороны, основное ценностное предложение перечислений (IMHO) заключается в том, что они _closed_/"final" (то есть нерасширяемые), так что что-то вроде оператора switch может предполагать тотальность. (Таким образом, возможность AdvEvents _расширить_ то, что значит быть BasicEvent , нарушает своего рода наименьший сюрприз для перечислений.)

Я не думаю, что вы можете использовать более двух из следующих трех свойств:

  • Перечисления закрыты/окончательны/предсказуемо итоги
  • Отношение extends между двумя объявлениями enum
  • (Разумное) предположение, что если b является экземпляром B и B extends A , то b является экземпляром A

@masak Я понимаю и согласен с закрытым принципом перечислений (во время выполнения). Но расширение времени компиляции не будет нарушать принцип закрытости во время выполнения, поскольку все они будут определены и созданы компилятором.

(Разумное) предположение, что если b является экземпляром B и B расширяет A, то b является экземпляром A

Я думаю, что это рассуждение вводит в заблуждение, поскольку дихотомия экземпляр/класс на самом деле не может быть назначена перечислению. Перечисления не являются классами и не имеют экземпляров. Однако я думаю, что они могут быть расширены, если все сделано правильно. Думайте о перечислениях как о множествах. В этом примере B является надмножеством A. Поэтому разумно предположить, что любое значение в A присутствует в B, но только НЕКОТОРЫЕ значения B будут присутствовать в A.

Я понимаю, откуда беспокойство... хотя. И я не уверен, что с этим делать. Хороший пример проблемы с расширением enum:

const enum A { a = 'a' }
const enum B extends A { b = 'b' }

const foo = (a: A) => console.log(a);
const bar = (b: B) => foo(b);

bar(B.a); // 'a'
bar(B.b); // uh-oh, b doesn't exist on A, so foo would get unexpected behavior

// HOWEVER, this would work just fine...

const baz = (a: A) => bar(a);

baz(A.a); // 'a'
baz(B.a); // 'a'
baz(B.b); // compiler error as expected...

В этом случае перечисления ведут себя совершенно иначе, чем классы. Если бы это были классы, вы могли бы довольно легко преобразовать B в A, но здесь это явно не сработает. Я не обязательно думаю, что это ПЛОХО, я думаю, что это нужно просто учитывать. IE, вы не можете охватывать тип перечисления вверх по его дереву наследования, как класс. Это может быть объяснено ошибкой компилятора, например, «невозможно присвоить перечисление надмножества B для A, поскольку не все значения B присутствуют в A».

@julian-sf

Я думаю, что это рассуждение вводит в заблуждение, поскольку дихотомия экземпляр/класс на самом деле не может быть назначена перечислению. Перечисления не являются классами и не имеют экземпляров.

Вы абсолютно правы, на первый взгляд.

  • Рассматриваемое как независимая языковая конструкция, перечисление имеет _members_, а не экземпляры. Термин «член» используется как в Справочнике, так и в Спецификации языка. (C# и Python аналогичным образом используют термин «член». В Java используется «константа перечисления», потому что «член» имеет в Java перегруженное значение.)
  • С точки зрения скомпилированного кода перечисление имеет _свойства_, отображающие оба пути — имена-значения и значения-имена. Опять же не экземпляры.

Размышляя об этом, я понимаю, что я немного окрашен подходом Java к перечислениям. В Java значения перечисления буквально являются экземплярами своего типа перечисления. С точки зрения реализации перечисление — это класс, расширяющий класс Enum . (Вам не разрешено делать это вручную , вам нужно пройти через ключевое слово enum , но это то, что происходит под капотом.) Хорошая вещь в этом заключается в том, что перечисления получают все удобства, которые делают классы: они могут иметь поля, конструкторы, методы... В этом подходе члены перечисления _являются_ экземплярами. (JLS говорит об этом.)

Обратите внимание, что я не предлагаю никаких изменений в семантике перечислений TypeScript. В частности, я не говорю, что TypeScript должен перейти на использование модели Java для перечислений. Я _am_ говорю, что поучительно/полезно накладывать «понимание» класса/экземпляра поверх членов перечислений/перечислений. Не «перечисление _является_ классом» или «член перечисления _является_ экземпляром»... но есть сходство, которое сохраняется.

Какие сходства? Прежде всего, тип членства.

enum Foo { A, B, C }
enum Bar { X, Y, Z }

let foo: Foo = Foo.C;
foo = Bar.Z;

Последняя строка не проверяет тип, потому что Bar.Z не является Foo . Опять же, это не _фактически_ классы и экземпляры, но это можно _понять_, используя ту же модель, как если бы Foo и Bar были классами, а шесть членов были их соответствующими экземплярами.

(Для целей этого аргумента мы проигнорируем тот факт, что let foo: Foo = 2; также является семантически допустимым, и что в целом значения number могут быть присвоены переменным типа enum.)

Перечисления имеют дополнительное свойство, заключающееся в том, что они _closed_ — извините, я не знаю лучшего термина для этого — как только вы их определите, вы не сможете их расширить. В частности, члены, перечисленные внутри объявления перечисления, являются _только_ вещами, тип которых соответствует типу перечисления. («Закрыто» как в «гипотезе закрытого мира».) Это отличное свойство, потому что вы можете с полной уверенностью убедиться, что все случаи в выражении switch в вашем перечислении были покрыты.

С extends в перечислениях это свойство выходит из окна.

Ты пишешь,

Я понимаю и согласен с закрытым принципом перечислений (во время выполнения). Но расширение времени компиляции не будет нарушать принцип закрытости во время выполнения, поскольку все они будут определены и созданы компилятором.

Я не думаю, что это правда, потому что предполагается, что любой код, который расширяет ваше перечисление, находится в вашем проекте. Но сторонний модуль может расширить ваше перечисление, и внезапно появляются _new_ члены перечисления, которые также могут быть назначены вашему перечислению вне контроля кода, который вы компилируете. По сути, перечисления больше не будут закрываться даже во время компиляции.

Я все еще чувствую, что несколько неуклюже выражаю то, что имею в виду, но я считаю, что это важно: extends на enum нарушило бы одну из самых ценных особенностей перечислений, тот факт, что они закрыто. Пожалуйста, подсчитайте, сколько языков абсолютно _запрещают_ расширять/подклассировать перечисление именно по этой причине.

Я еще немного подумал об обходных путях, и, отработав #17592 (комментарий) , вы можете добиться большего, также определив Events в пространстве значений:

enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

let e: Events = Events.Pause;

Из моего тестирования видно, что Events.Start правильно интерпретируется как BasicEvents.Start в системе типов, поэтому проверка полноты и уточнение размеченных объединений работают нормально. Главное, чего не хватает, это то, что вы не можете использовать Events.Pause в качестве литерала типа; вам нужно AdvEvents.Pause . Вы _можете_ использовать typeof Events.Pause , и он разрешается в AdvEvents.Pause , хотя люди в моей команде были сбиты с толку таким шаблоном, и я думаю, что на практике я бы поощрял AdvEvents.Pause при использовании это как тип.

(Это для случая, когда вы хотите, чтобы типы перечислений могли назначаться между собой, а не между отдельными перечислениями. По моему опыту, чаще всего требуется, чтобы они были назначаемыми.)

Я думаю, что это самое изящное решение на данный момент.

Спасибо @alangpierce :+1:

какие-либо обновления по этому поводу?

@sdwvit Я не один из основных людей, но, с моей точки зрения, следующее предложение по синтаксису (из OP, но повторно предложенное дважды после этого) сделает всех счастливыми без каких-либо известных проблем:

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

Это сделало бы _меня_ счастливым, потому что это означало бы реализацию этой, казалось бы, полезной функции «дублировать все элементы в этом другом перечислении» _без_ использования extends , что я считаю проблематичным по причинам, которые я изложил. Синтаксис ... позволяет избежать этих проблем за счет копирования, а не расширения.

Проблема по-прежнему помечена как «Ожидает дальнейших отзывов», и я уважаю право основных участников сохранять ее в этой категории до тех пор, пока они считают это необходимым. Но также ничто не мешает кому-либо реализовать вышеизложенное и представить его в качестве PR.

@masak спасибо за ответ. Теперь мне нужно пройти через всю историю обсуждения. Вернемся к вам после :)

Мне бы очень хотелось, чтобы это произошло, и я бы очень хотел попытаться реализовать это сам. Однако нам все еще нужно определить поведение для всех перечислений. Все это хорошо работает для строковых перечислений, но как насчет ванильных числовых перечислений. Как здесь работает расширение/копирование?

  • Я предполагаю, что мы захотим разрешить расширение перечисления только перечислением «того же типа» (числовое расширяет числовое, строка расширяет строку). Гетерогенные перечисления технически поддерживаются, поэтому я полагаю, что мы должны сохранить эту поддержку.

  • Должны ли мы разрешать расширение из нескольких перечислений? Должны ли они все иметь взаимоисключающие ценности? Или мы допустим перекрывающиеся значения? Приоритет на основе лексического порядка?

  • Могут ли расширенные перечисления переопределять значения расширенных перечислений?

  • Должны ли расширенные перечисления появляться в начале списка значений или они могут быть в любом порядке? Я предполагаю, что более поздние определенные значения имеют более высокий приоритет?

  • Я предполагаю, что неявные числовые значения будут продолжаться с 1 после максимального значения расширенных числовых перечислений.

  • Особые соображения для битовых масок?

и т.д.

@JeffreyMercado Это хорошие вопросы, и они подходят для тех, кто надеется попытаться реализовать. :улыбка:

Ниже приведены мои ответы, основанные на «консервативном» подходе к проектированию (например, «давайте примем проектные решения, которые не допускают случаев, в которых мы не уверены, вместо того, чтобы делать выбор сейчас, который трудно изменить позже, оставаясь при этом обратно совместимым»).

  • Я предполагаю, что мы захотим разрешить расширение перечисления только перечислением «того же типа» (числовое расширяет числовое, строка расширяет строку)

Я тоже так предполагаю. Полученное перечисление смешанного типа не кажется супер-полезным.

  • Должны ли мы разрешать расширение из нескольких перечислений? Должны ли они все иметь взаимоисключающие ценности? Или мы допустим перекрывающиеся значения? Приоритет на основе лексического порядка?

Поскольку мы говорим о семантике копирования, дублирование нескольких перечислений кажется «более приемлемым», чем множественное наследование в стиле C++. Я не сразу вижу в этом проблему, особенно если мы продолжим аналогию с разбросом объектов: let newEnum = { ...enumA, ...enumB };

Должны ли все члены иметь взаимоисключающие значения? Консервативным было бы сказать «да». Опять же, аналогия с распространением объектов дает нам альтернативную семантику: побеждает последний.

Я не могу придумать ни одного случая использования, в котором я был бы признателен за возможность переопределить значения перечисления. Но это может быть просто недостаток воображения с моей стороны. Консервативный подход к запрещению коллизий имеет приятные свойства, которые легко объяснить/усвоить, и, по крайней мере, теоретически он может выявить реальные ошибки проектирования (в свежем коде или коде, который поддерживается).

  • Могут ли расширенные перечисления переопределять значения расширенных перечислений?

Я думаю, что ответ и аргументация в этом случае во многом такие же, как и в предыдущем случае.

  • Должны ли расширенные перечисления появляться в начале списка значений или они могут быть в любом порядке? Я предполагаю, что более поздние определенные значения имеют более высокий приоритет?

Сначала я собирался сказать, что это имеет значение только в том случае, если мы используем семантику переопределения «выигрывает последний».

Но, если подумать, как в случае «без коллизий», так и в случае «выигрывает последний», я нахожу странным даже _хочу_ помещать объявления членов перечисления до того, как перечисление распространится в списке. Например, какое намерение сообщается при этом? Спреды немного похожи на «импорт», и они обычно идут вверху.

Я не обязательно хочу запрещать помещать enum-спреды после объявлений членов enum (хотя я думаю, что я был бы в порядке, если бы это было запрещено в грамматике). Если в конечном итоге это будет разрешено, это определенно то, что линтеры и соглашение сообщества могут указать как предотвратимые. Просто нет веских причин для этого.

  • Я предполагаю, что неявные числовые значения будут продолжаться с 1 после максимального значения расширенных числовых перечислений.

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

  • Особые соображения для битовых масок?

Я думаю, что это будет охвачено вышеуказанным правилом.

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

export enum Unit {
    SECONDS,
    MINUTES,
    HOURS,
    DAYS,
    WEEKS,
    MONTHS,
    YEARS,
    DECADES,
    CENTURIES,
    MILLENNIA
}

interface Labels {
    SINGULAR: Record<Unit, string>
    PLURAL: Record<Unit, string>
    LAST: string;
    DELIM: string;
    NOW: string;
}

export const EnglishLabels: Labels = {
    SINGULAR: {
        [Unit.SECONDS]: ' second',
        [Unit.MINUTES]: ' minute',
        [Unit.HOURS]: ' hour',
        [Unit.DAYS]: ' day',
        [Unit.WEEKS]: ' week',
        [Unit.MONTHS]: ' month',
        [Unit.YEARS]: ' year',
        [Unit.DECADES]: ' decade',
        [Unit.CENTURIES]: ' century',
        [Unit.MILLENNIA]: ' millennium'
    },
    PLURAL: {
        [Unit.SECONDS]: ' seconds',
        [Unit.MINUTES]: ' minutes',
        [Unit.HOURS]: ' hours',
        [Unit.DAYS]: ' days',
        [Unit.WEEKS]: ' weeks',
        [Unit.MONTHS]: ' months',
        [Unit.YEARS]: ' years',
        [Unit.DECADES]: ' decades',
        [Unit.CENTURIES]: ' centuries',
        [Unit.MILLENNIA]: ' millennia'
    },
    LAST: ' and ',
    DELIM: ', ',
    NOW: ''
}

@illeatmyhat Это хорошее использование перечислений, но ... я не понимаю, как это считается расширением существующего перечисления. То, что вы делаете, это _using_ перечисление.

(Кроме того, в отличие от операторов enum и switch, кажется, что в вашем примере у вас нет проверки целостности; кто-то, кто позже добавил элемент перечисления, мог легко забыть добавить соответствующий ключ в SINGULAR и PLURAL запись во всех экземплярах Label .)

@масак

кто-то, кто позже добавил элемент перечисления, мог легко забыть добавить соответствующий ключ в записи SINGULAR и PLURAL во всех экземплярах Label .)

По крайней мере, в моей среде выдается ошибка, когда элемент перечисления отсутствует либо SINGULAR , либо PLURAL . Думаю, тип Record делает свое дело.

Хотя документация для TS хороша , я чувствую, что не так много примеров того, как нетривиальным образом объединить множество функций. enum inheritance было первым, что я искал, когда пытался решить проблемы интернационализации, что привело к этой теме. Подход все равно оказался неправильным, поэтому я и написал этот пост.

@illeatmyhat

По крайней мере, в моей среде выдается ошибка, когда элемент перечисления отсутствует либо SINGULAR , либо PLURAL . Думаю, тип Record делает свое дело.

Ой! ТИЛ. И да, это делает его намного интереснее. Я понимаю, что вы имеете в виду, когда говорите о первоначальном стремлении к наследованию перечисления и, в конечном итоге, о своем шаблоне. Это может быть даже не единичный случай; «Проблемы X/Y» — это реальная вещь. Многие люди могут начать с мысли «Я хочу расширить MyEnum », но в конечном итоге использовать Record<MyEnum, string> , как это сделали вы.

Ответ @masak :

С расширениями на перечисления это свойство выходит из окна.

Ты пишешь,

@julian-sf: я понимаю и согласен с закрытым принципом перечислений (во время выполнения). Но расширение времени компиляции не будет нарушать принцип закрытости во время выполнения, поскольку все они будут определены и созданы компилятором.

Я не думаю, что это правда, потому что предполагается, что любой код, который расширяет ваше перечисление, находится в вашем проекте. Но сторонний модуль может расширить ваше перечисление, и вдруг появятся новые члены перечисления, которые также могут быть назначены вашему перечислению вне контроля кода, который вы компилируете. По сути, перечисления больше не будут закрываться даже во время компиляции.

Чем больше я об этом думаю, тем больше вы совершенно правы. Перечисления должны быть закрыты. Мне очень нравится идея «составлять» перечисления, так как я думаю, что это действительно суть того, что мы хотим здесь 🥳.

Я думаю, что эта нотация довольно элегантно резюмирует концепцию «сшивания» двух отдельных перечислений:

enum ComposedEnum = { ...EnumA, ...EnumB }

Так что считайте, что я отказываюсь от использования термина extends 😆


Комментарии к ответам @masak на вопросы @JeffreyMercado :

  • Я предполагаю, что мы захотим разрешить расширение перечисления только перечислением «того же типа» (числовое расширяет числовое, строка расширяет строку). Гетерогенные перечисления технически поддерживаются, поэтому я полагаю, что мы должны сохранить эту поддержку.

Я тоже так предполагаю. Полученное перечисление смешанного типа не кажется супер-полезным.

Хотя я согласен с тем, что это бесполезно, нам, вероятно, СЛЕДУЕТ сохранить гетерогенную поддержку перечислений здесь. Я думаю, что здесь было бы полезно предупреждение о линтере, но я не думаю, что TS должен этому мешать. Я могу придумать надуманный вариант использования, который заключается в том, что я создаю перечисление для взаимодействия с очень плохо разработанным API, который принимает флаги, представляющие собой смесь чисел и строк. Надумано, я знаю, но поскольку это разрешено в других местах, я не думаю, что мы должны запрещать это здесь.

Может быть, просто сильное поощрение не делать этого?

  • Должны ли мы разрешать расширение из нескольких перечислений? Должны ли они все иметь взаимоисключающие ценности? Или мы допустим перекрывающиеся значения? Приоритет на основе лексического порядка?

Поскольку мы говорим о семантике копирования, дублирование нескольких перечислений кажется «более приемлемым», чем множественное наследование в стиле C++. Я не сразу вижу в этом проблему, особенно если мы продолжим аналогию с распространением объектов: let newEnum = { ...enumA, ...enumB };

100% согласен

  • Должны ли все члены иметь взаимоисключающие значения?

Консервативным было бы сказать «да». Опять же, аналогия с распространением объектов дает нам альтернативную семантику: побеждает последний.

Я разрываюсь здесь. Хотя я согласен с тем, что «лучшей практикой» является обеспечение взаимной исключительности ценностей, правильно ли это? Это прямо противоречит общеизвестной семантике распространения. С одной стороны, мне нравится идея применения взаимоисключающих значений, с другой стороны, она разрушает множество предположений о том, как должна работать семантика распространения. Есть ли какие-либо недостатки в следовании обычным правилам распространения с «выигрывает последний»? Кажется, что это проще в реализации (поскольку базовый объект в любом случае является просто картой). Но это также, кажется, совпадает с общими ожиданиями. Я склоняюсь к тому, чтобы меньше удивляться.

Также могут быть хорошие примеры переопределения значения (хотя я понятия не имею, что это может быть).

Но, если подумать, как в случае «без коллизий», так и в случае «выигрывает последний», мне кажется странным даже хотеть помещать объявления членов перечисления до того, как перечисление распространится в списке. Например, какое намерение сообщается при этом? Спреды немного похожи на «импорт», и они обычно идут вверху.

Ну, это зависит от того, если мы следуем семантике распространения, то порядок не должен иметь значения. Честно говоря, даже если бы мы применяли взаимоисключающие значения, порядок не имел бы большого значения, верно? В этой точке столкновение будет ошибкой, независимо от порядка.

  • Я предполагаю, что неявные числовые значения будут продолжаться с 1 после максимального значения расширенных числовых перечислений.

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

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

@julian-sf

Так что считайте, что моя отставка по поводу использования термина продлевается 😆

:+1: Общество Сохранения Суммовых Типов аплодирует со стороны.

Но, если подумать, как в случае «без коллизий», так и в случае «выигрывает последний», мне кажется странным даже хотеть помещать объявления членов перечисления до того, как перечисление распространится в списке. Например, какое намерение сообщается при этом? Спреды немного похожи на «импорт», и они обычно идут вверху.

Ну, это зависит от того, если мы следуем семантике распространения, то порядок не должен иметь значения. Честно говоря, даже если бы мы применяли взаимоисключающие значения, порядок не имел бы большого значения, верно? В этой точке столкновение будет ошибкой, независимо от порядка.

Я говорю, что «нет веских причин размещать спреды после обычных объявлений членов»; вы говорите, что «при соответствующих ограничениях размещение их до или после не имеет значения». Обе эти вещи могут быть истинными одновременно.

Основное различие в результатах, по-видимому, заключается в разрешении или запрещении распространения перед обычными членами. Это может быть синтаксически запрещено; он может выдать предупреждение линтера; или это может быть совершенно нормально во всех отношениях. Если порядок не имеет семантического значения, тогда все сводится к тому, чтобы функция распространения enum следовала принципу наименьшего удивления , проста в использовании и проста в обучении/объяснении.

Использование оператора распространения приводит к более широкому использованию поверхностного копирования в JS и TypeScript. Это, безусловно, более широко используемый и простой для понимания метод, чем использование extends , которое подразумевает прямую связь. Создание перечисления через композицию было бы более простым решением для использования.

Некоторые предложения обходного пути, хотя и действительные и полезные, добавляют гораздо больше шаблонного кода для достижения того же желаемого результата. Учитывая окончательный и неизменный характер перечисления, было бы желательно создать дополнительные перечисления посредством композиции, чтобы сохранить свойства, совместимые с другими языками.

Жалко только, что 3 года на этот разговор еще идет.

@ jmitchell38488 Я бы поставил лайк вашему комментарию, но ваше последнее предложение изменило мое мнение. Это необходимое обсуждение, поскольку предлагаемое решение будет работать, но оно также подразумевает возможность расширения классов и интерфейсов таким образом. Это большое изменение, которое может отпугнуть некоторых программистов, использующих языки C++, от использования машинописного текста, поскольку в конечном итоге вы получите два способа сделать одно и то же ( class A extends B и class A { ...(class B {}) } ). Я думаю, оба способа могут поддерживаться, но тогда нам нужно extend для перечислений, а также для согласованности.

@masak wdyt? ^

@sdwvit Я не говорю об изменении поведения при создании классов и интерфейсов, я говорю конкретно о перечислениях и их создании с помощью композиции. Это неизменяемые окончательные типы, поэтому у нас не должно быть возможности расширяться обычным способом наследования.

Учитывая природу JS и конечное переданное значение, нет никаких причин, по которым композиция не может быть достигнута. Конечно, работа с перечислениями стала бы более привлекательной.

@masak wdyt? ^

Хм. Я думаю, что согласованность языка — похвальная цель, и поэтому не будет _a priori_ неправильным просить аналогичную функцию ... в классах и интерфейсах. Но я думаю, что дело здесь слабее или отсутствует, и по двум причинам: (а) классы и интерфейсы уже имеют механизм расширения, и добавление второго дает небольшую дополнительную ценность (тогда как предоставление одного для перечислений охватило бы вариант использования, который люди возвращались к этому вопросу в течение многих лет); (b) добавление нового синтаксиса и семантики в классы имеет гораздо более высокую планку одобрения, поскольку классы в некотором смысле взяты из спецификации EcmaScript. (TypeScript старше ES6, но были предприняты активные усилия, чтобы первый оставался ближе ко второму. Это включает в себя отказ от добавления дополнительных функций сверху.)

Я думаю, что эта ветка была открыта в течение долгого времени просто потому, что она представляет собой достойную функцию, которая будет охватывать реальные варианты использования, но для нее еще не создано PR. Создание такого PR требует времени и усилий, больше, чем просто сказать, что вы хотите эту функцию. :подмигивание:

Кто-нибудь работает над этой функцией?

Кто-нибудь работает над этой функцией?

Я предполагаю, что нет, так как мы еще даже не закончили обсуждение этого вопроса, ха-ха!

Я чувствую, что мы приблизились к консенсусу относительно того, как это будет выглядеть. Поскольку это будет добавление языка, вероятно, потребуется «чемпион», чтобы продвигать это предложение. Я думаю, что кто-то из команды технического обслуживания должен прийти и переместить проблему из «Ожидается отзыв» в «Ожидает предложения» (или что-то подобное).

Я работаю над его прототипом. Хотя я не очень далеко продвинулся из-за нехватки времени и незнания структуры кода. Я действительно хочу довести это до конца, и если не будет другого движения, я продолжу, когда смогу.

Также понравилась бы эта функция. Прошло 37 месяцев, надеюсь, скоро будет достигнут прогресс

Заметки с недавней встречи:

  • Нам нравится синтаксис расширения, потому что extends подразумевает подтип, тогда как «расширение» перечисления создает супертип.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • Мысль состоит в том, что AdvEvents.Start разрешается в идентификатор того же типа, что и BasicEvents.Start . Это означает, что типы BasicEvents.Start и AdvEvents.Start можно присваивать друг другу, а тип BasicEvents можно присваивать AdvEvents . Надеюсь, это имеет интуитивно понятный смысл, но важно отметить, что это означает, что спред — это не просто синтаксический ярлык — если мы расширим спред до того, как он выглядит:

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     Start = "Start",
     Finish = "Finish",
     Pause = "Pause",
     Resume = "Resume"
    }
    

    это имеет другое поведение — здесь BasicEvents.Start и AdvEvents.Start _нельзя_ присваивать друг другу из-за непрозрачного качества строковых перечислений.

    Другим незначительным последствием этой реализации является то, что AdvEvents.Start , вероятно, будет сериализован как BasicEvents.Start в кратких сведениях и выдаче объявлений (по крайней мере, там, где нет синтаксического контекста, связывающего член с AdvEvents — наведение курсора на буквальное выражение AdvEvents.Start может привести к быстрой информации, которая говорит AdvEvents.Start , но, тем не менее, может быть более понятно отображать BasicEvents.Start ).

  • Это разрешено только в строковых перечислениях.

Я хотел бы попробовать этот.

Чтобы уточнить: это не одобрено для реализации.

Здесь есть два возможных варианта поведения, и оба они кажутся плохими.

Вариант 1: это на самом деле сахар

Если распространение действительно означает то же самое, что и копирование элементов расширенного перечисления, то будет большим сюрпризом, когда люди попытаются использовать значение расширенного перечисления, как если бы оно было значением расширенного перечисления, и это не сработает.

Вариант 2: на самом деле это не сахар

Предпочтительным вариантом было бы, чтобы распространение работало больше как предложение @aj-r по типу объединения в верхней части темы. Если это поведение, которое люди уже хотят, то существующие варианты на столе кажутся строго предпочтительными для понятности. В противном случае мы создаем новый тип строкового перечисления, которое не ведет себя как любое другое строковое перечисление, что странно и, кажется, подрывает «простоту» предложения здесь.

Какого поведения хотят люди и почему?

Я не хочу вариант 1, потому что он приводит к большим сюрпризам.

Мне нужен вариант 2, но я хотел бы иметь достаточную поддержку синтаксиса, чтобы преодолеть упомянутый недостаток @aj-r, чтобы я мог написать let e: Events = Events.Pause; из его примера. Недостаток не страшен, но это место, где расширенное перечисление не может скрыть реализацию; так что как-то мерзко.

Я тоже думаю, что вариант 1 - плохая идея. Я искал ссылки на эту проблему в своей компании и нашел два обзора кода, где она была связана, и в обоих случаях (и в моем личном опыте) существует явная необходимость в том, чтобы элементы меньшего перечисления можно было назначать большему типу перечисления. . Я особенно беспокоюсь о том, что один человек, представляющий ... , думает, что он ведет себя как вариант 2, а затем следующий человек действительно запутывается (или прибегает к хакам, таким как as unknown as Events.Pause ), когда более сложные случаи не работают.

Уже есть много способов попытаться получить поведение варианта 2: множество фрагментов в этой теме, а также различные подходы, включающие объединения строковых литералов. Мое большое беспокойство по поводу реализации варианта 1 заключается в том, что он эффективно представляет еще один неправильный способ получения варианта 2 и, таким образом, приводит к большему устранению неполадок и разочарованию людей, изучающих эту часть TypeScript.

Какого поведения хотят люди и почему?

Поскольку код говорит громче слов, и на примере из OP:

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause", // We added a new field
    Finish = "Finish2" // Oops, we actually modified a field in the parent enum
};
// The TypeScript compiler should refuse to compile this code
// But after removing the "Finish2" line,
// the TypeScript compiler should successfully handle it as one would normally expect with the spread operator

Этот пример демонстрирует вариант 2, верно? Если так, то я отчаянно хочу второй вариант.

В противном случае мы создаем новый тип строкового перечисления, который не ведет себя как любое другое строковое перечисление, что странно и, кажется, подрывает «простоту» предложения здесь.

Я согласен с тем, что вариант 2 немного тревожит, и, возможно, лучше сделать шаг назад и подумать об альтернативах. Вот исследование того, как это можно сделать без добавления какого-либо нового синтаксиса или поведения к сегодняшним перечислениям:

Я думаю, что мое предложение в https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 в наши дни ближе всего и может быть чем-то, над чем можно поработать:

type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

Я вижу две основные проблемы с этим подходом:

  • Это действительно сбивает с толку того, кто изучает TypeScript; если у вас нет четкого представления о пространстве типов и пространстве значений, похоже, что он перезаписывает тип константой.
  • Он неполный, поскольку не позволяет использовать Events.Pause в качестве типа.

Ранее я предлагал https://github.com/microsoft/TypeScript/issues/29130 , который (среди прочего) отвечал бы второму пункту списка, и я думаю, что он все еще может быть ценным, хотя он, безусловно, добавит много тонкости к как работают имена.

Одной из синтаксических идей, которая, как мне кажется, поможет решить оба вопроса, является альтернативный синтаксис const , который также объявляет тип:

// Declares a value and a type at the same time with the same name (just like `enum` and `class` already do).
// Requires the right-hand side to be either a unit type or an object where all values are unit types.
// The JS emit would just take out the word "type" and leave everything else.
const type Events = {...BasicEvents, ...AdvEvents};
...
const e: Events.Pause = Events.Pause;
...
// The syntax could also make this pattern more ergonomic.
const type INACCESSIBLE = "INACCESSIBLE";
const response: {name: string, favoriteColor: string} | INACCESSIBLE = INACCESSIBLE;

Это стало бы ближе к миру, где перечисления вообще не нужны. (Мне всегда казалось, что перечисления идут вразрез с целями дизайна TS, поскольку они представляют собой синтаксис уровня выражения с нетривиальным поведением генерации и номинальным по умолчанию.) Синтаксис объявления const type также позволит вам создайте структурно-типизированное строковое перечисление следующим образом:

const type BasicEvents = {
  Start: 'Start',
  Finish: 'Finish',
};  // "as const" would be implicit for "const type" declarations.

По общему признанию, способ, которым это должно работать, заключается в том, что тип BasicEvents является сокращением для typeof BasicEvents[keyof typeof BasicEvents] , что может быть слишком сфокусировано для синтаксиса с общим именем, такого как const type . Возможно, другое ключевое слово было бы лучше. Жаль, что const enum уже занято 😛.

Исходя из этого, я думаю, что единственным разрывом между (строковыми) перечислениями и литералами объектов будет номинальная типизация. Возможно, это можно решить с помощью синтаксиса, такого как as unique или const unique type , который в основном использует номинальное типизированное поведение для этих типов объектов, а в идеале также и для обычных строковых констант. Это также дало бы четкий выбор между вариантом 1 и вариантом 2 при определении Events : вы используете unique , когда хотите, чтобы Events был совершенно другим типом (вариант 1), и вы опускаете unique , если хотите получить возможность присваивания между Events и BasicEvents (вариант 2).

С const type и const unique type был бы способ чистого объединения существующих перечислений, а также способ выразить перечисления как естественную комбинацию функций TS, а не одноразовую.

Что тут происходит? 😅

вау из 2017 🤪, какие еще отзывы вам нужны?

какие еще отзывы вам нужны?

Прямо здесь??? Мы реализуем предложения не только потому, что они устарели!

какие еще отзывы вам нужны?

Прямо здесь??? Мы реализуем предложения не только потому, что они устарели!

да. Я не был уверен, каким образом это было.

Читаю снова и вижу также #40998 Я думаю, что это лучший способ... эмуны - это объекты, и я думаю, что распространение легче объединить/расширить перечисления.

Я не думаю, что я достаточно квалифицирован, чтобы высказать свое мнение о языковом дизайне, но я думаю, что могу оставить отзыв как обычный разработчик.

Я столкнулся с этой проблемой пару недель назад в реальном проекте, где хотел использовать эту функцию. В итоге я использовал подход @alangpierce . К сожалению, из-за моих обязанностей перед работодателем я не могу поделиться здесь кодом, но вот несколько моментов:

  1. Повторение объявлений (как type, так и const для нового перечисления) не было большой проблемой и не сильно ухудшало читабельность.
  2. В моем случае enum представлял разные действия для определенного алгоритма, и в этом алгоритме были разные наборы действий для разных ситуаций. Использование иерархии типов позволило мне убедиться, что определенные действия не могут происходить во время компиляции: в этом была суть всего и оказалось весьма полезным.
  3. Код, который я написал, был внутренней библиотекой, и хотя различие между различными типами перечислений было важно внутри библиотеки, оно не имело значения для внешних пользователей этой библиотеки. Используя этот подход, я смог экспортировать только один тип, который был типом суммы всех различных перечислений внутри, и скрыть детали реализации.
  4. К сожалению, мне не удалось придумать идиоматический и легко читаемый способ автоматического анализа значений типа суммы из объявлений типа. (В моем случае различные этапы алгоритма, о которых я упоминал, загружались из базы данных SQL во время выполнения). Это не было серьезной проблемой (написание кода синтаксического анализа вручную было достаточно простым), но было бы неплохо, если бы реализация наследования перечислений уделяла этому вопросу некоторое внимание.

В целом, я думаю, что многие реальные проекты со скучной бизнес-логикой выиграют от этой функции. Разделение перечислений на разные подтипы позволило бы системе типов проверять множество инвариантов, которые сейчас проверяются модульными тестами, а сделать неверные значения непредставимыми системой типов — это всегда хорошо.

Привет,

Позвольте мне добавить свои два цента здесь 🙂

Мой контекст

У меня есть API с документацией OpenApi, созданной с помощью tsoa .

Одна из моих моделей имеет статус, определенный следующим образом:

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

У меня есть метод setStatus , который принимает подмножество этих статусов. Поскольку эта функция недоступна, я решил продублировать перечисление таким образом:

enum RequestedEntityStatus {
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
}

Итак, мой метод описывается так:

public setStatus(status: RequestedEntityStatus) {
   this.status = status;
}

с этим кодом я получаю эту ошибку:

Conversion of type 'RequestedEntityStatus' to type 'EntityStatus' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

что я сейчас и сделаю, но мне было любопытно, и я начал искать этот репозиторий, когда нашел это.
Прокрутив весь путь вниз, я не нашел никого (или, может быть, я что-то пропустил), который предлагал бы этот вариант использования.

В моем случае использования я не хочу «расширяться» из перечисления, потому что нет причин, по которым EntityStatus будет расширением RequestedEntityStatus. Я бы предпочел иметь возможность «выбирать» из более общего перечисления.

Мое предложение

Я нашел оператор распространения лучше, чем предложение с расширениями, но я хотел бы пойти дальше и предложить следующее:

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

enum RequestedEntityStatus {
    // Pick/Reuse from EntityStatus
    EntityStatus.started,
    EntityStatus.paused,
    EntityStatus.stopped,
}

// Fake enum, just to demonstrate
enum TargetStatus {
    {...RequestedEntityStatus},
    // Why not another spread here?
    //{...AnotherEnum},
    EntityStatus.archived,
}

public class Entity {
    private status: EntityStatus = 'created'; // Why not a cast here, if types are compatible, and deserializable from a JSON. EntityStatus would just act as a type union here.

    public setStatus(requestedStatus: RequestedEntityStatus) {
        if (this.status === (requestedStatus as EntityStatus)) { // Should be OK because types are compatible, but the cast would be needed to avoid comparing oranges and apples
            return;
        }

        if (requestedStatus == RequestedStatus.stopped) { // Should be accessible from the enum as if it was declared inside.
            console.log('Stopping...');
        }

        this.status = requestedStatus;// Should work, since EntityStatus contains all the enum members that RequestedEntityStatus has.
    }

    public getStatusAsStatusRequest() : RequestedEntityStatus {
        if (this.status === EntityStatus.created || this.status === EntityStatus.archived) {
            throw new Error('Invalid status');
        }
        return this.status as RequestedEntityStatus; // We have  eliminated the cases where the conversion is impossible, so the conversion should be possible now.
    }
}

В общем, это должно работать:

enum A { a = 'a' }
enum B { a = 'a' }

const a:A = A.a;
const b:B = B.a;

console.log(a === b);// Should not say "This condition will always return 'false' since the types 'A' and 'B' have no overlap.". They do have overlaps

Другими словами

Я хотел бы ослабить ограничения на типы перечислений, чтобы они больше походили на союзы ( 'a' | 'b' ), чем на непрозрачные структуры.

Добавляя эти возможности в компилятор, два независимых перечисления с одинаковыми значениями могут быть назначены друг другу по тем же правилам, что и объединения:
Учитывая следующие перечисления:

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C { ...A, c = 'c' }

И три переменные a:A , b:B и c:C

  • c = a должно работать, потому что A — это просто подмножество C, поэтому каждое значение A является допустимым значением C
  • c = b должно работать, поскольку B равно 'a' | 'c' , оба из которых являются допустимыми значениями C.
  • b = a может работать, если известно, что a отличается от 'b' (что соответствует только типу 'a' )
  • a = b также может работать, если известно, что b отличается от 'c'
  • b = c может работать, если известно, что c отличается от 'b' (что будет равно 'a'|'c' , что и есть B)

или, может быть, нам нужно явное приведение правых частей, что касается сравнения на равенство?

О конфликтах членов перечисления

Я не поклонник правила «последних побед», даже если оно кажется естественным с оператором спреда.
Я бы сказал, что компилятор должен возвращать ошибку, если дублируется либо «ключ», либо «значение» перечисления, если только ключ и значение не идентичны:

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C {...A, ...B } // OK, equivalent to enum C { a = 'a', b = 'b', c = 'c' }, a is deduplicated
enum D {...A, a = 'd' } // Error : Redefinition of a
enum E {...A, d = 'a' } // Error : Duplicate key with value 'a'

Закрытие

Я нахожу это предложение довольно гибким и естественным для работы разработчиков TS, в то же время обеспечивая большую безопасность типов (по сравнению с as unknown as T ) без фактического изменения того, что такое перечисление. Он просто представляет новый способ добавления элементов в перечисление и новый способ сравнения перечислений друг с другом.
Как вы думаете? Я пропустил какую-то очевидную проблему языковой архитектуры, которая делает эту функцию недостижимой?

Была ли эта страница полезной?
0 / 5 - 0 рейтинги