У Typescript є об’єднання, тож чи перерахування зайві?


83

З тих пір, як TypeScript представив типи об'єднань, мені цікаво, чи є якась причина оголошувати тип перерахування. Розглянемо наступну декларацію типу перерахування:

enum X { A, B, C }
var x:X = X.A;

і подібне оголошення типу об'єднання:

type X: "A" | "B" | "C"
var x:X = "A";

Якщо вони в основному служать одній і тій же меті, а профспілки більш потужні та виразні, то чому перелічення необхідні?


1
Перераховує карту чисел, що, на мою думку, може бути корисним у певних ситуаціях. Я припускаю, що вони також хочуть з часом зробити більше з переліченнями, наприклад, надати їм властивості, які ви можете викликати (наприклад, c # або перерахування Java.)
PaulBGD,

Відповіді:


69

Наскільки я бачу, вони не є зайвими, через дуже просту причину, що типи об'єднання - це суто поняття часу компіляції, тоді як перерахування насправді трансліруються і потрапляють у отриманий javascript ( зразок ).

Це дозволяє робити деякі речі з переліченнями, які в іншому випадку неможливі для типів об'єднання (наприклад, перерахування можливих значень перерахування )


1
Я думаю, ви хочете сказати, що є причини використовувати enum. Перше речення заплутане. "Наскільки я бачу, [перелічення] не ..." відповідають на запитання "[є] будь-яка причина оголосити тип перерахування."
Меттью

1
під час мого читання перше речення "Наскільки я бачу, що це не так" мав на увазі переліки у титульному запитанні "... так чи перерахунки зайві?" . я
подаю редакцію

51

В останніх версіях TypeScript легко оголосити типові типи об’єднання. Тому вам слід віддавати перевагу типам об’єднань перед переліченнями.

Як оголосити ітеративні типи об’єднань

const permissions = ['read', 'write', 'execute'] as const;
type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute'

// you can iterate over permissions
for (const permission of permissions) {
  // do something
}

Коли фактичні значення типу об'єднання не описують себе дуже добре, ви можете назвати їх так само, як і з переліками.

// when you use enum
enum Permission {
  Read = 'r',
  Write = 'w',
  Execute = 'x'
}

// union type equivalent
const Permission = {
  Read: 'r',
  Write: 'w',
  Execute: 'x'
} as const;
type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x'

// of course it's quite easy to iterate over
for (const permission of Object.values(Permission)) {
  // do something
}

Не пропустіть as constтвердження, яке відіграє вирішальну роль у цих моделях.

Чому не добре використовувати перелічення?

1. Неперервні переліки не відповідають поняттю "набраний набір JavaScript"

Я думаю, що ця концепція є однією з найважливіших причин, чому TypeScript став настільки популярним серед інших мов altJS. Non-const перелічення порушують концепцію, випускаючи об'єкти JavaScript, які живуть під час виконання, із синтаксисом, який не сумісний з JavaScript.

2. У переліках Const є деякі підводні камені

Перерахування Const не можна перенести за допомогою Babel

На даний момент існує два способи вирішення цієї проблеми: позбутися const enum вручну або за допомогою плагіна babel-plugin-const-enum.

Оголошення const enum у зовнішньому контексті може бути проблематичним

Переміщення навколишнього середовища не дозволяється, коли надається --isolatedModulesпрапор. Член команди TypeScript каже, що " const enumдля DT насправді не має сенсу" (DT відноситься до DefinitelyTyped) і "Ви повинні використовувати об'єднаний тип літералів (рядок або число) замість" const enum в контексті оточення.

Const enum під --isolatedModulesпрапором поводяться дивно навіть поза зовнішнім контекстом

Я був здивований, прочитавши цей коментар на GitHub, і підтвердив, що поведінка все ще відповідає дійсності з TypeScript 3.8.2.

3. Числові переліки не є безпечними для типу

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

enum ZeroOrOne {
  Zero = 0,
  One = 1
}
const zeroOrOne: ZeroOrOne = 2; // no error!!

4. Оголошення рядкових переліків може бути зайвим

Іноді ми бачимо такий тип перерахувань рядків:

enum Day {
  Sunday = 'Sunday',
  Monday = 'Monday',
  Tuesday = 'Tuesday',
  Wednesday = 'Wednesday',
  Thursday = 'Thursday',
  Friday = 'Friday',
  Saturday = 'Saturday'
}

Я повинен визнати, що існує функція перерахування, яка не досягається за типами об’єднань

Навіть якщо з контексту очевидно, що значення рядка включено до переліку, ви не можете призначити його перерахуванню.

enum StringEnum {
  Foo = 'foo'
}
const foo1: StringEnum = StringEnum.Foo; // no error
const foo2: StringEnum = 'foo'; // error!!

Це уніфікує стиль присвоєння значення перелічення в коді, виключаючи використання рядкових значень або рядкових літералів. Ця поведінка не узгоджується з тим, як поводиться система типу TypeScript в інших місцях, і це якось дивно, і деякі люди, які вважали, що це слід виправити, порушували проблеми ( це і це ), в яких неодноразово згадувалося, що намір перерахувань рядків є забезпечити "непрозорі" типи рядків: тобто їх можна змінювати, не змінюючи споживачів.

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// As this style is forced, you can change the value of
// Weekend.Saturday to 'Sat' without modifying consumers
const weekend: Weekend = Weekend.Saturday;

Зверніть увагу, що ця "непрозорість" не є досконалою, оскільки присвоєння значень перерахування типам рядкових літералів не обмежується.

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// The change of the value of Weekend.Saturday to 'Sat'
// results in a compilation error
const saturday: 'Saturday' = Weekend.Saturday;

Якщо ви вважаєте, що ця "непрозора" функція настільки цінна, що ви можете прийняти всі описані вище недоліки в обмін на неї, ви не можете відмовлятися від перерахувань рядків.

Як усунути перелічення з вашої кодової бази

З no-restricted-syntaxправилом ESLint, як описано .


ОК здорово. Але що, якщо ви хочете помістити цю декларацію у спільний файл набору символів d.ts? Ви можете ініціалізувати const до рядка або числового літералу лише в оточуючому контексті
prpm

1
Я думаю, що неможливо переглядати щось, що декларується лише в оточуючому контексті. Забезпечення як оголошення, так declare const permissions: readonly ['read', 'write', 'execute']і реального об’єкта JavaScript const permissions = ['read', 'write', 'execute']має працювати.
kimamula

1
Я не хочу, щоб рядкові літерали були призначуваними та порівнянними з Enums. const foo: StringEnum === StringEnum.Foo // true or false Я хочу, щоб це обмеження переконалось, що ми не маємо змішань буквальних рядків.
метью

1
> Шаблон об'єднання типів набагато крутіший @kimamula це слабкий аргумент. Це виходить від прихильника шаблону, запропонованого у цій інакше дуже добре поставленій відповіді.
манінак

1
@maninak Дякую за ваш коментар. Я погоджуюсь і видалив цю частину.
kimamula

29

Є кілька причин, за якими ви можете захотіти використовувати enum

Я бачу великими перевагами використання об’єднання в тому, що вони забезпечують стислий спосіб представлення цінності з кількома типами, і вони дуже читаються. let x: number | string

EDIT: станом на TypeScript 2.4 Enums тепер підтримує рядки.

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
} 

5
Чи я єдиний, хто вважає, що використання enumдля "бітових прапорів" є анти-шаблоном?
Cameron Tacklind

15

Перелічення можна концептуально розглядати як підмножину типів об'єднань, присвячених intта / або stringцінностей, з кількома додатковими функціями, згаданими в інших відповідях, які роблять їх зручними для використання, наприклад, простір імен .

Що стосується безпеки типу, числові перерахування є менш безпечними, тоді йдуть типи об’єднання і, нарешті, перерахування рядків:

// Numeric enum
enum Colors { Red, Green, Blue }
const c: Colors = 100; // ⚠️ No errors!

// Equivalent union types
type Color =
    | 0 | 'Red'
    | 1 | 'Green'
    | 2 | 'Blue';

let color: Color = 'Red'; // ✔️ No error because namespace free
color = 100; // ✔️ Error: Type '100' is not assignable to type 'Color'

type AltColor = 'Red' | 'Yellow' | 'Blue';

let altColor: AltColor = 'Red';
color = altColor; // ⚠️ No error because `altColor` type is here narrowed to `"Red"`

// String enum
enum NamedColors {
  Red   = 'Red',
  Green = 'Green',
  Blue  = 'Blue',
}

let namedColor: NamedColors = 'Red'; // ✔️ Error: Type '"Red"' is not assignable to type 'Colors'.

enum AltNamedColors {
  Red    = 'Red',
  Yellow = 'Yellow',
  Blue   = 'Blue',
}
namedColor = AltNamedColors.Red; // ✔️ Error: Type 'AltNamedColors.Red' is not assignable to type 'Colors'.

Детальніше про цю тему в цій статті 2ality: Переліки TypeScript: Як вони працюють? Для чого їх можна використовувати?


Типи об'єднань підтримують неоднорідні дані та структури, уможливлюючи поліморфізм, наприклад:

class RGB {
    constructor(
        readonly r: number,
        readonly g: number,
        readonly b: number) { }

    toHSL() {
        return new HSL(0, 0, 0); // Fake formula
    }
}

class HSL {
    constructor(
        readonly h: number,
        readonly s: number,
        readonly l: number) { }

    lighten() {
        return new HSL(this.h, this.s, this.l + 10);
    }
}

function lightenColor(c: RGB | HSL) {
    return (c instanceof RGB ? c.toHSL() : c).lighten();
}

Між переліченнями та типами об’єднання одиночні можуть замінювати перерахування. Це більш детально, але також і більш об’єктно-орієнтовано :

class Color {
    static readonly Red   = new Color(1, 'Red',   '#FF0000');
    static readonly Green = new Color(2, 'Green', '#00FF00');
    static readonly Blue  = new Color(3, 'Blue',  '#0000FF');

    static readonly All: readonly Color[] = [
        Color.Red,
        Color.Green,
        Color.Blue,
    ];

    private constructor(
        readonly id: number,
        readonly label: string,
        readonly hex: string) { }
}

const c = Color.Red;

const colorIds = Color.All.map(x => x.id);

Я схильний дивитись на F #, щоб побачити хороші практики моделювання. Цитата зі статті про F # переліки на F # для розваги та отримання прибутку, яка може бути тут корисною:

Загалом, вам слід віддавати перевагу дискримінованим типам об'єднань перед переліченнями, якщо вам дійсно не потрібно мати значення int (або а string), пов'язане з ними

Існують інші альтернативи перелічуванню моделей. Деякі з них добре описані в цій статті 2 Альтернативи переліченням у TypeScript .


0

Тип перерахування не є зайвим, але в більшості випадків перевага є об'єднанням.

Але не завжди. Використання перелічень для представлення, наприклад, переходів стану може бути набагато зручнішим та виразнішим, ніж використання об'єднання **

Розглянемо реальний реальний сценарій:

enum OperationStatus {
  NEW = 1,
  PROCESSING = 2,
  COMPLETED = 4
}

OperationStatus.PROCESSING > OperationStatus.NEW // true
OperationStatus.PROCESSING > OperationStatus.COMPLETED // false

2
Думаю, якщо ви зробите це, вам слід чітко призначити числові значення в декларації переліку, щоб запобігти переупорядкуванню (наприклад, тому, що хтось вважає, що було б більш розумно мати їх в алфавітному порядку) із-за помилки виконання, яку можна пропустити.
dpoetzsch
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.