Comments 34
Я — евангелист строковых Enum. В статье, ключи отличаются от значений — я считаю это за абсолютно нереальный случай. Согласен, что так писать нельзя. И всё же:
1) Удобно дебажить (удачи в каких-нибудь React Devtools узнать значение в props, когда в Enum 20-30 значений)
2) Спокойно можно добавлять новые значения, менять местами (можно перзистить значения и спать спокойно, зная, что ничего не сломается)
3) Меньше размер бандла после компиляции (не катастрофа, но всё же)
// обычный Enum
// результат = {0: "BAR", 1: "BAZ", BAR: 0, BAZ: 1}
var Foo;
(function (Foo) {
Foo[Foo["BAR"] = 0] = "BAR";
Foo[Foo["BAZ"] = 1] = "BAZ";
})(Foo || (Foo = {}));
// строковый Enum
// результат {BAZ: "BAZ", FOO: "FOO"}
var Bar;
(function (Bar) {
Bar["BAZ"] = "BAZ";
Bar["FOO"] = "FOO";
})(Bar || (Bar = {}));
Магические геттеры/сеттеры… Хочется вернуться в прошлое и не допустить их появление в JS.
Просто писать явно getSomething и setSomething.
пп. 1 и 2 — бесценны.
Абсолютно непонятно, почему автор оригинального текста решил предпочесть числовые enum. Потом ему кто-то (или даже он сам) добавляет новый элемент в начало enum — и привет, все enum-проверки сломались.
type FooBar = 'foo' | 'bar'
someVar = GENRE.ROMANTIC
дает более полное представление о контексте применения кода, чем
someVar = 'romantic'
Хотя бы потому, что романтическим может быть что угодно — от девушек до самолётов.
более полное представление о контексте применения кода, чем
someVar = 'romantic'
let someVar: FooBar = 'foo'; // ok
let someOtherVar: FooBar = 'baz'; // compile error
Какое еще «более полное представление» вам нужно? Бороться с duck typing в JS/TS не имеет ни малейшего смысла.
Обилие строковых литералов в коде на мой взгляд неудобно.
В последнем комментарии я имел в виду читабельность кода. Мы смотрим на коммит, там изменилась одна строчка. По выбранному enum мы видим и понимаем смысл кода быстрее, чем просто по строковому литералу.
Если вы читаете мало коммитов, для вас это может быть не актуально.
Можно извратиться и совместить, как описано тут:
const FooBar = {
FOO = "foo",
BAR = "bar",
} as const;
type FooBar = (typeof FooBar)[keyof typeof FooBar];
function Baz(f: FooBar) { ... } // using FooBar type
Baz(FooBar.FOO); // using FooBar's value
Умный поиск/рефакторинг в IDE гораздо лучше работает с явным enum, чем со строковыми константами.
Со строковыми литералами компилятор, конечно, выдаст ошибку, если рефакторинг был произведен некорректно. Но с enum рефакторинг просто сразу будет корректным.
Idea отлично следит за union types
Хм. А и правда. Такое ощущение, что ситуация сильно улучшилась по сравнению тем, что было еще год назад.
Есть нюанс, правда.
const x = "string-from-type"; // type: "string"
выводится как string
(естественно). Нужно явно указывать тип:
const x: MyType = "string-from-type"; // type: "MyType"
В случае с enum автоматически выведенный тип переменной будет по как раз типом этого enum
const x = MyType.stringFromType; // type: "MyType"
Тут можно возразить, что хрен редьки не слаще и вообще дело вкуса.
В этом случае ещё может помочь as const
, которое отключает авторасширение типа: const x = 'value'; // x: 'value'
. Работает с любыми значениями, а свойства в объектах ещё и помечает как readonly
Да, в Java с начала нулевых сложилась адовая практика добавлять на каждое поле класса геттеры и сеттеры. Это приводит к тому, что код дико разбухает, а заявленная гибкость требуется только для ничтожного проекта случаев. А 95 процентов геттеров и сеттеров — это просто мертвый лишний код.
Практика показала, что геттеры и сеттеры в таком примитивном виде — не удобное решение. Гораздо делать отдельно чистые дата-классы и чистые объекты.
Что такое чистый дата-класс? Это класс без логики, без методов, просто набор типизированных полей.
class Circle {
radius: number;
perimeter: numer;
surface: number;
}
Эти классы используются для передачи и хранения данных. Модификацию полей в них — и всю логику вычисления — производят специальные классы, только у которых есть доступ. Формулы вычислений содержит, скажем, специальный GeometryTransformer.
Это даёт настоящую гибкость, так как геометрию можно теперь спокойно менять на лету, подставив просто нужный объект GeometryTransformer — хоть евклидовый, хоть Лобачевского. При этом все остальное не переписывается и даже не пересоздаётся — чистый hot swap, великолепная возможность кардинально изменить правила игры и любых расчетов на лету.
Кроме дата-классов, можно использовать классы, в которых могут быть методы, очень похожие по смыслу на геттеры и сеттеры, к примеру,
class Shop {
getGoodList(): Array<Good>
}
При размышлении и написании таких классов, особенно при разработке прототипа, может показаться, что перед нами типичный геттер. Более того, в прототипе для теста такой класс действительно может содержать приватное поле со списком товаров goodList.
Но есть нюанс — об этом не надо думать, как о геттере, то есть об обертке над полем класса. Это метод, контракт для других классов, которые будут работать с Shop. А откуда магазин берет список товаров — это его личное дело. Там может быть поле. А может быть и обращение к другому классу.
Краткие итоги:
1. Не думайте «геттеры» и «сеттеры» — думайте о сокрытии реализации
2. Для хранения данных используйте data-классы как хранилища полей, без методов
3. Логика связи полей у дата-класса, вынесенная во внешний «трансформер-вычислитель» — сразу закладывает в вашу архитектуру не только гибкость, но и возможность менять формулы и поведение на лету, с теми же коллекциями объектов, на том же экране!
Эти классы используются для передачи и хранения данных. Модификацию полей в них — и всю логику вычисления — производят специальные классы, только у которых есть доступ. Формулы вычислений содержит, скажем, специальный GeometryTransformer.
Для меня это выглядит как «давайте возьмём плохое из ФП и ООП, объединим, и будем пользоваться».
Но такая композиция часто выручает в разработке игр, особенно когда могут меняться правила.
более хорошим и естественным примером дата-класса, который просто несет в себе информацию, это
class Point{
x: number;
y: number;
}
В таком примере очевидно, что для такого класса геттеры-сеттеры избыточны, так как модель точки в пространстве не подразумевает взаимосвязи координат (но такая взаимосвязь возникает, если мы начинаем думать о функциях типа y =f(x).) Все зависит от выбранной модели. В данном кейсе Point — это просто коллекция x и y.
Геттеры в js удобны тем, что если сделать очепятку в имени, то программа рухнет с исключением. Если использовать свойства, то будет молча пропускать:
const prog = { }
if (!prog.hasBug()) { // упадет с исключением
}
if (!prog.hasBug) { // всегда true
}
Напоминает typeclass из хаскелей. Есть адаптация для тайпскрипта fp-ts.
Какие то вредные советы.
const GENRE = {
ROMANTIC: 0x01,
DRAMA: 0x02,
COMEDY: 0x04,
DOCUMENTARY: 0x08,
};
movie.genre = GENRE.ROMANTIC | GENRE.COMEDY;
лучше так
enum GENRE {
ROMANTIC = 1,
DRAMA = 2,
COMEDY = 4,
DOCUMENTARY = 8
}
Тогда уж лучше так:
enum GENRE {
ROMANTIC = 0b0001,
DRAMA = 0b0010,
COMEDY = 0b0100,
DOCUMENTARY = 0b1000
}
А вообще, без нормальных средств дебага возня с битами лишь усложняет поддержку.
Ну и прелесть битовых флагов как раз в том и состоит, что потом в коде поштучной возни с битами никакой и нет. Там чисто логические функции: проверить флаг &, добавить |. Всё наглядно и просто. И при этом одновременно компактно и эффективно.
только при отладке придется смотреть на этот список или заучить его наизусть )
В дополнение к уже написанному, про Enum.
Встречал при изучении языка ещё одно мнение и решил его придерживаться — вообще не использовать этот тип. Мысль была в том, что TS стоит рассматривать как типизированный JS (JS+типы), который не оставляет следа в рантайме, Enum же, напару с Namespaces, генерируют свой код. И в том, что любые навыки, приобретённые при разработке на TS, должны остаться полезными и при возврате на JS (ничто не вечно под луной).
Ещё одна причина — мнимая типобезопасность при использовании не-строковых enum. Если я хочу, чтобы значением было что-то из enum, то я действительно хочу этого, а не вот этого. Строковые enum в этом плане работают куда более ожидаемо.
Ну и в случае, где мне хотелось бы использовать enum, мои потребности покрывает юнион строк.
Он исчезнет, а его используемые значения заинлайнятся
1. пример (не уверен, что сохранится)
2. документация
radius: number;
— плохо в обоих случаях. Не надо в JS/TS тащить это всё матёрое ООП. На него даже в дотнетах и джавах уже забили.
Ну, про enum уже обсудили выше. Пройдемся по остальным пунктам.
Избегайте проверки типов
- Код "хорошо" и "плохо" вообще разный — у классов разный интерфейс же. Совет звучит как "используйте полиморфизм вместо условного выполнения кода". Ничего Typescript-специфичного тут нет.
Используйте итераторы и генераторы
- А если вот мне не нужная ленивая генерация коллекции? Ну и опять же — а причем тут Typescript?
Используйте геттеры и сеттеры
- Опять нет ничего Typescript-специфичного
- Персональное мнение — категорически против. Это делает код менее очевидным: почему чтение или запись вот конкретно этого свойства класса вдруг привело к сайд-эффектам? При явном вызове методов это будет сюрпризом гораздо в меньшей степени.
Создавайте объекты с приватными/защищенными полями
- Применимо практически к любому ОО-языку (ладно, с натяжкой можно засчитать — ведь JS модификаторы доступа пока не поддерживает)
"почему чтение или запись вот конкретно этого свойства класса вдруг привело к сайд-эффектам?"
Ну, например, потому что требуется некая консистентность состояния объекта. А внешнему классу о ней знать не полагается (инуапсуляция-с). Поэтому вы не думаете о методе, когда пользуетесь типом со свойствами, так что очевидность только растёт.
Кроме того, свойства можно писать без опоры на поля. Тогда при использовании типа можно будет обращать внимание только на свойство как значение, а при его разработке не заморачиваться хранением и актуализацией, а оставить всё на откуп вычислимому свойству. Иногда очень удобно
Чистый код для TypeScript — Часть 1