🎬 Вступление

О чем эта книга

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

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

🅰 Именование

Дать имя и не остаться в дураках

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

Предлагаю пойти "от противного" и посмотреть для начала как не нужно делать.

Если бы мы знали что это такое, но мы не знаем что это такое

Почему у разработчиков тяга к бесполезным префиксам и постфиксам, а так же к сильно обобщенным именованиям - не известно.

Давайте рассмотрим добавление префикса/постфикса:

const userData: User = {
  id: number
  name: string
}

Когда я спрашиваю, что означает Data на конце, к сожалению в лучшем случае я получают ответ "данные". Если я получаю такой ответ, то задаю вопрос, почему у него на каждой переменной не висит префикс/постфикс data. Обычно люди понимают, что это бесполезная штука, мне каждый раз приходится это напоминать, ведь привычка очень долго выкорчёвывается. Тоже самое касается и info.

Рассмотрим следующий пример:

async function foo(value: User) {
  ...
  const object = convert(value)
  ...
  const data = await request(object.query)
  if (data.error) {
    return setError(...)
  }
  ...
  setData(data, object.bar)
}

Когда я читаю такой код, я сразу представляю, как такой человек рассказывает про свои действия в таком ключе:

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

Так же отмечу что я ​Так никто не общается, все говорят "мы с друзьями сели в машину", "решили поесть", "пододвинули стулья и сели за стол", "официант принес нам еду" и т.д. Обычно человек в своей речи использует слова, которые конкретизируют то, что они хотят сказать.

Перепишем функцию избавившись от неверного именования:

async function foo(user: User) {
  ...
  const { query, bar } = convert(user)
  ...
  const response = await request(query)
  if (response.error) {
    return setError(...)
  }
  ...
  setResponse(response, bar)
}

Данный пример содержит условности, но отражает суть. Не нужно обобщать, если вы работаете над конкретными сущностями.

Обобщения нужны в том случае, если вы не знаете с чем конкретным будет работать ваш код. Например есть метод map у массива. Мы не знаем какие данные конкретно будет использовать пользователей нашей функции. Давайте ее напишем.

function map<A, B>(array: A[], ab: (item: A) => B): B[] {
    const result = [] as B[];
    for (let index = 0; index < array.length; index++) {
        result.push(ab(array[index]));
    }

    return result;
}

Рассмотрим имена и начнем с аргументов. array - это просто массив, мы не знаем, что это за массив, мы можем работать с любым массивом, единственное ограничение и конкретика здесь, то что это должен быть массив. ab - это функция которая преобразует a -> b. Потому тут не используется слово "callback", которое может означать абсолютно любую функцию. У данной функции есть прямая задача и здесь отражается суть. Можно назвать более полно - convertAToB, transformAToB и т.д. Я выбрал более короткое имя, оно имеет стандарт в мире функционального программирования. Так как это чистая функция, с именем map - которое пришло к нам их этого мира, я решил использовать их стандарты.

Далее имя result - оно нам говорит о том, что это результат функции. Так как у нее довольно четкие границы и больше, к тому же данная переменная прибита к for - человек сразу понимает, что это и зачем. Так как нет информации, какие элементы будут в результате, так и остается result.

Я не сокращаю index до i. Об этом мы поговорим чуть ниже.

⌛️ Скрщния

Одна из самых болезненных вещей при прочтении кода - это сокращения. Часто люди используют сокращения подсмотрев их на stackoverflow или в коде других людей. Лично я избегаю сокращений максимально. Это делает код более целостным и заставляет больше думать над именами, которые могут получаться длинными из-за префиксов или постфиксов.

Давайте разберем частые сокращения:

const handleClick: MouseHandler = (e) => ...
const workWith = (i: number) => ...
const btnText = ...
const idx = ...
const l = array.length

Это популярные сокращения. За мою карьеру, даже я сталкивался с тем, что не понимал часть из них по коду. Например я все время думал что idx это тоже самое что и id, просто человек опечатался. Оказалось, что это index. Так же я сталкивался с сокращением i, которое уходило за рамки for и становилось головной болью, потому что какой-то другой разработчик мог начать использовать функцию так, что передавал не index, а какое-то другое число в функцию, которая принимала i и это приводило к скрытым ошибкам.

Так же, всякие сокращения по типу e внезапно могли начать использоваться не как event, а как emitter, или еще какая-то другая сущность. В связи с этим я выработал следующее правило:

Сокращения из одной буквы - это чистое зло. Сокращения по типу btn чуть меньшее зло.

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

🔨 Функционируем

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

const checkIsGreaterOrEqual = () => {};

models.user.convertToString();

На самом деле в некоторых случаях применимо именование toString(), или isEqual(). Куда будет приятнее читать вот так:

const isGreaterOrEqual = (...) => {}

isDisabled={isGreaterOrEqual(foo, bar)}

models.user.toString()
models.user.isValid()
models.user.toPeer()

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

🙉 Слушаем и обрабатываем

Почему-то у людей при написании обработчиков событий случается буря вариантов.

Например:

const onClick = () => {};
const clickHandler = () => {};
const handleClick = () => {};

<div onClick={fn} />;

Давайте просто почитаем.

  • onClick - на клик onClick={onClick} - на клик на клик
  • clickHandler - обработчик клика onClick={clickHandler} на клик обработчик клика
  • handleClick - обработать клик onClick={handleClick} на клик обработать клик

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

На самом деле все довольно просто, если вы создаете функцию, которая будет обрабатывать какое-то событие, то это всегда приставка handle:

const handleClick = () => {};

<div onClick={handleClick} />;

Если вы хотите сообщить что-то, то это приставка on, что символизирует событие. Например React использует пропсы с префиксом on в стандартных элементах, для работы с событиями. Элементы React сами по себе не обрабатывают события, они как бы сообщают через callback, что что-то произошло, например клик.

Так же одно событие может порождать другое событие или обработчик порождать какое-либо событие, например возьмем React компонент:

export type Props = {
  onChange?: (event: KeyboardEvent) => void
}
export const SomeComponent = ({ onChange }: Props) => {
  // ...
  return <input type="text" onKeyDown={onChange} />
}

// ---------------------------------------------------

export type Props = React.PropsWithChildren<{
  id: number
  onSelect?: (id: number) => void
}>

export const SomeComponent = ({ id, children, onSelect}: Props) => {
  const handleClick = () => {
    // ...
    onSelect?.(id)
  }
  return <button type="text" onClick={handleClick}>{children}</button>
}

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

🌴 Принцип контекстов

Объединяй и властвуй

Очень часто можно наблюдать, как разработчики пишут код в таком духе:

export const findUser = (id: number) => {};
export const checkUserExists = (id: number) => {};
export const userValidate = (user: User) => {};
export const getUserName = (id: number) => {};

Проблема в том, что здесь явно прослеживается связь функций - это сущность User. Это означает, что можно эти функции объединить под один контекст.

export const user = {
    find(id: number) {},
    checkExists(id: number) {},
    validate(user: User) {},
    getName(id: number) {},
};

as user from ..., вот пример файла user/user.ts:

export const find = (id: number) => {};
export const checkExists = (id: number) => {};
export const validate = (user: User) => {};
export const getName = (id: number) => {};

Файл user/index.ts:

export * as user from './user';

Теперь это можно использовать следующим образом:

const name = user.getName(id);

// ...

if (user.validate(admin)) {
}

Кошелечек в сумочке, сумочка в мешочке

Контексты могут быть вложенными! Вложенные контексты очень удобны. Представим такую ситуацию, что у нас несколько разных сущностей - User, Post, Comment

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

helpers.user.someFunction / helpers.post.someFunction ...
store.users / store.posts / store.comments
selectors.users / selectors.posts / selectors.comments

Второй вариант:

users.selectors. / users.herlpers. / users.store /
posts.selectors. / posts.herlpers. / posts.store /
comments.selectors. / comments.herlpers. / comments.store /

Два варианта никак не противоречат друг другу. Более того, иногда можно встретить обе варианта в одном проекте и это не будет считаться ошибкой. Например в какой то момент store может быть неудобно хранить в сущности users / posts / comments и потребуется инвертирование, более того, могут накладываться ограничения используемых технологий.

Эй вы, трое, а ну сюда оба подошли, да, я тебе говорю!

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

Проще простого

Первое - именование происходит от одиночной сущности и там где требуется множественное, используется префикс или постфикс:

// user.ts

export const find = (id: number) => {};
export const validate = (user: User) => {};
export const getName = (id: number) => {};

// many
export const findMany = (ids: number[]) => {};
export const getNames = (ids: number[]) => {};
export const everyValid = (users: User[]) => {};

Шиворот на выворот

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

// users.ts

export const find = (ids: number[]) => {};
export const validate = (user: User[]) => {};
export const getName = (ids: number[]) => {};

// single
export const findOne = (id: number) => {};
export const getNameOne = (id: number) => {};
export const validOne = (users: User) => {};

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

И волки сыты и овцы целы

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

type User = {
    id: number;
    firstName: string;
    lastName: string;
};

type Users = Record<number, User>;
type UserList = User[];

Соответственно по данному примеру у меня будет примерно такая структура:

selectors.user.find(id);
selectors.users.find(ids);
selectors.userList.find(ids);

helpers.userList.convert.toUsers(userList); // -> Record<number, User>

Мне кажется это самым правильным и чистым решением.

😫 Enum - oh shit were go again

Давайте поговорим здесь про enum'ы в typescript. Как можно догадаться из названия - я не очень хорошо отношусь к этой структуре. Казалось бы, удобная вещь, можно использовать значение как тип, не нужно указывать значение - TS любезно сгенерирует чиселки.

Но как только вы начинаете работать в большом проекте, можно наткнуться на не очень приятные последствия.

Беда не приходит одна

Я шота не понял...

Представим такую ситуацию, создается компонент:

export enum ButtonSize {
    Small,
    Medium,
    Large,
}

export type Props = {
    size: ButtonSize;
};

export function Button(props: Props) {
    return <button />;
}

Используется он в каком то другом месте:

return <Button size={ButtonSize.Large} />;

С первого взгляда все хорошо, но давайте создадим enum, который будет абсолютной копией enum ButtonSize. Положим в отдельный файл:

// some.ts

export enum Size {
    Small,
    Medium,
    Large,
}

Теперь этот enum попытаемся использовать как значение для Button:

<Button size={Size.Large} />

Внезапно получается ошибка:

Type 'Size.Large' is not assignable to type 'ButtonSize'.ts(2322)

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

// some.ts

export enum ButtonSize {
    Small,
    Medium,
    Large,
}

То теперь вдруг все начинает работать! Это непоследовательное поведение создает проблемы при рефакторинге.

Иногда это мешает, из-за например автогенерации кода, например я значение получаю от сервера и хотел бы использовать его напрямую, без небезопасного каста через as. С size это конечно тяжело представить, но вот если у вас компонент например User, а у него есть поле type, которое имеет тип перечисления, а с сервера приходит поле role, с enum типом, которое было автоматически сгенерировано с именем Role, и при этом они зеркальны - я бы хотел использовать их напрямую и получать ошибку только, если типы реально несовместимы.

Впихуется в невпихуемое

Здесь речь пойдет по такие страшные слова как ковариантность, контравариантность, инвариантность. Ну вот понтанулся и больше я их говорить не хочу.

Будем говорить проще и понятнее. Любой программист понимает, что есть более широкий тип, а есть более узкий тип. Например в ООП, более широким типом считается родитель, а дети его более узким вариантом. В мат части это называется "суммой типов" - это широкий, а единичный тип - это узкий, проще говоря когда вы тип описываете через вертикальную черту |, то это сумма, и он широкий, а узкий, это каждый член этого типа.

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

Пример:

type Role = 'admin' | 'user'

const hasPermission = (role: Role) => {/*...*/}

// ...

type User = {
  role: string // обратим внимание, тип более широкий чем Role
}

const user: User;

// type error
if (hasPermission(user.role)) {
  // ...
}

// -------------------------------------------------

// type guard
const isRole = (str: string) str is Role => str === 'admin' || str === 'user'

const { role } = user
// this is correct
if (isRole(role) && hasPermission(role)) {
  // ...
}

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

И зачем я сделал эти прелюдии?

А вот зачем, объявим переменную size с типом number и используем в качестве значения для пропсы size компонента Button. Ваши ставки господа:

const size: number = 20

<Button size={size} /> // this is fine

Это нормально для TS. более того, ему и с NaN будет хорошо:

export default function App() {
    const size = NaN
    return (
        <div className="App">
            <Button size={size}>
        </div>
    )
}

Закомпилился

А теперь нужно посмотреть во что же компилируется enum:

export var ButtonSize;
(function (ButtonSize) {
    ButtonSize[(ButtonSize['Small'] = 0)] = 'Small';
    ButtonSize[(ButtonSize['Medium'] = 1)] = 'Medium';
    ButtonSize[(ButtonSize['Large'] = 2)] = 'Large';
})(ButtonSize || (ButtonSize = {}));

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

Для меня здесь самая большая проблема - это то, что он генерирует обратный маппинг. Зачем, а главное почему? Если я хочу сделать цикл без фильтрации мне не обойтись, а если enum достаточно крупный, то это удвоение может сыграть на струнах вашего перфоманса.

Итоги

Enum - с виду красивое и удобное решение, но имеет ряд недостатков:

  • Небезопасен в рантайме
  • Небезопасен на уровне типизации
  • Непоследовательное и непредсказуемое поведение
  • Генерирует лишний мусор

А шо теперь делать?

Есть два решения - сумма типов и const enum. Я же придерживаюсь первого и сейчас расскажу как. Для начала я создаю просто объект:

export const sizes = {
    small: 0,
    medium: 1,
    large: 2,
} as const;

На конце обязательно должен быть as const, иначе в значениях будет number, или string, что нам не подходит.

Один раз в каком-то глобальном d.ts файле необходимо прописать такой тип-хелпер:

declare type ValueOf<T> = T[keyof T];

Я его указываю в глобальном дифинишине чтоб не таскать каждый раз импорт. Он работает как keyof, но только по значениям объекта. Использование:

export type Props = {
    size: ValueOf<typeof sizes>;
};

Теперь посмотрим на использование:

<Button size={sizes.large} /> // fine
<Button size={1} /> // work

const size: number = NaN;
<Button size={size} /> // type error

const size: number = sizes.large;
<Button size={size} /> // type error

Теперь не получается обмануть тип и подсунуть ему любое число без каких-либо кастов. Давайте посмотрим, как будет работать если мы создадим отдельный список размеров:

// some.ts
export const someSizes = {
  small: 0,
  medium: 1,
  large: 2,
  extraLarge: 3
} as const;


// App.tsx

<Button size={someSizes.small} /> // fine, works
<Button size={someSizes.extraLarge} /> // type error

Давайте попробуем тип ValueOf<typeof someSizes> использовать для пропса size:

// Создадим функцию, потому что просто переменная там не подойдет, потому TS кастит тип к примитиву при использовании
const getSize = (): ValueOf<typeof someSizes> => someSizes.large

// large совместим с Button, но возвращаемый тип будет 0 | 1 | 2 | 3
<Button size={getSize()} /> // type error

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

export const sizes = {
    small: 'small',
    medium: 'medium',
    large: 'large',
} as const;

type Size = ValueOf<typeof sizes>;

const isSuperSize = (size: Size) => size === 'large';

Это же может помочь вам избавиться от импортов.

Так же удобно использовать цикл по таким объектам, ибо там нет лишнего.

А ну-ка отрефактори

Самый жирный козырь в рукаве у любителей enum'ов, как они думают - это рефакторинг. Но на самом деле это не так. Я ни разу не видел реального рефакторинга, в котором enum был на коне.

Данный тип легко подлежит рефакторингу через VSCode пример (F2). Делается это и правда не так красиво, сначала тип нужно будет преобразовать в то, что отдает ValueOf (IDE любезно подскажет):

type Size = 'small' | 'medium' | 'large';

Потом поставить курсор на нужном значении, нажать F2 и изменить значение, вернуть ValueOf. Изменить его в объекте и все снова будет работать. Но если вы откажитесь от использования литералов в остальном коде - этого минуса вообще не испытаете. Технически у вас остается тот же самый enum без недостатков.

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

🗿 Констатируем

Довольно часто можно увидеть недопонимание как должны выглядеть константы. Конечно для констант используется UPPER_CASE. Однако не все понимают что такое константа.

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

Для начала следует понять откуда растут ноги - константы пришли к нам в мир программирования из математики. Это неизменяемое значение, которое вообще никогда не меняется. Конечно мы не математики, и данный параметр может поменяться, но не во время исполнения программы - это очень важно. Константа всегда должна оставаться с одинаковым значением и начала и до конца ее жизни.

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

const CONST_1 = () => {}; // incorrect ❌

const CONST_2 = 10; // correct ✅

const ARR_CONST = [1, 2, 3, 4, 5]; // correct ✅

// incorrect: ❌
if (ARR_CONST.includes(2)) {
}

// correct: ✅
if (includes(ARR_CONST, 2)) {
}

const CONFIG = { x: 1, y: 2 }; // correct ✅
calculate(CONFIG.x, event); // incorrect ❌
calculate(CONFIG, event); // correct ✅

// incorrect: ❌
const CFG = {
    FOO: {},
    BAR: 12,
};

// correct: ✅
const cfg = {
    FOO: {},
    BAR: 12,
};

Из примеров видно, что константы могут быть использованы только целиком. Они могут быть вложенны друг в друга, потому что это контекст, как es6 модуль export * as config from './config' - такие вещи не именуются через UPPER_CASE никогда. В других языках это может быть использоваться с большой буквы.

🫨 На меня накричали!

Некоторые используют в опредении только "эта переменная не меняется в ходе программы, потому она константна". Функция или процедура - это минипрограмма, тогда по этому определению такой код является валидным:

const NUMERS = [1, 2, 3, 4, 5] as const;
const MY_SUPER_CALCULATION = (ADDITIONAL: number) => {
    const RESULT = NUMBERS.reduce((ACC, N) => ACC + N, 0) + NUMBERS;

    if (RESULT <= 0) {
        return undefined;
    }
    return RESULT;
};

NUMBERS - никогда не меняется. Значение переменной MY_SUPER_CALCULATION тоже - не забываем, это переменная у которой в значении лежит функция. Аргумент ADDITIONAL тоже никогда не меняется в ходе программы вычисления(функции). Дальнейшую логику я думаю вы уловили. В данном примере вообще никогда ничего не меняется, потому все подходит под это простое определение. Это крайний случай, но по частям данные проблемы начинают возникать на разных проектах.

🧐 Очевидно и явно

Очевидное неочевидно, а явное не явно

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

То что очевидно вам, может быть неочевидно другим, даже если вам очевидно, что ему тоже очевидно.

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

Магия вне Хогвартса

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

Чем механизм проще - тем он надежнее, и вряд ли кто-то его будет использовать неправильно.

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

Коперфильд

Для React/Vue/Angular разработчиков примером может быть внезапно появившийся props, которого он ниоткуда сам не передавал и не ожидал.

В классе появилось свойство из ниоткуда, а в функции родился неожиданный контекст.

Примеры:

function MySuperCoperfield({ children, ...props }: Props) {
    return React.cloneChildren(children, { ...props, type: 'wzhooh' });
}
handleMouse(event) {
    this.smt // work with this
}

// call with another context(this)
handleMouse.apply({}, ...args)

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

Павукан

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

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

// === file Foo
import * as bar from './bar';

export function fn() {
    bar.fn();
}

// === file Bar
import * as baz from './baz';

export function fn() {
    baz.fn();
}

// === file Baz
import * as foo from './foo';

export function fn() {
    foo.fn();
}

Соколиный глаз

Иногда встречаются конструкции циклами или условиями без скобок - это довольно багоопасная штука.

if (disabled) return;

while (condition) doIt();

for (let i = 0; i < arr.length; i++) doIt(arr[i]);

Кажется довольно безобидным, особенно вызод из функции через return, сколько таких можно встретить в open source, библиотеках, документации. Если можно с return, значит можно и с чем-то другим. Так же в if может быть куда более большая проверка. Две дополнительные строки не сделают погоды, зато сэкономят много времени другому разработчику.

Опасна эта конструкция тем, что можно упустить область работы блока.

Порефакторил и получил что-то такое

if (isValid(foo) && bar && exists(baz) && isSuper && type === 'submit') return;

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

Язык rust специально не стал добавлять такой способ работы блочных вычислений.

Что говоря про отрицание - часто лучше напрямую сравнивать, нежели неявно преобразовывать через !.

// ❌ BAD:
if (!isSome) {
}
if (!INTERNAL) {
}

// ✅ Good:
if (isSome === false) {
}
if (INTERNAL === false) {
}

В таких случаях вы сразу видите, что это boolean и он проверяется на false. Инвертирование в голове в целом не очень быстро проходит и люди мысленно "спотыкаются" при прочтении на отрицании в проверках.

Пожалуй единственное, где я использую отрицание - это на опциональных значених, которые может быть object | null | undefined. Делать две проверки вместо одной еще хуже сказывается на чтении.

Можете считать меня слепым, но мне иногда сложно заметить ! или отсутсвие {} у if. Из-за несоблюдения данных правил, я создавал баги при рефакторинге чужого кода.

😘 Не усложняй

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

Все ...сложна, переделывай!

Начнем сразу с примера.

function move(key: number) {
    let x = 0;
    let y = 5;
    if (key === 0) {
        x = 10;
        y = -10;
    }
    if (key === 1) {
        y -= 50;
    }
    return {
        x,
        y,
    };
}

Данный код имеет ряд проблем. И самая главная проблема, что здесь используется let. Вторая, кажется, что несколько if могут выполниться последовательно, но на самом деле это не так. Тертье - это разный способ рабы с переменными, во втором блоке почему-то используется прибавление.

Как это можно переделать? Избавляемся от let и можно вернуть сразу значение. А чтобы сохранить дефолтное поведение создается переменная:

const defaultMove = {
    x: 0,
    y: 5, // gravity
};
function move(move: number) {
    if (key === 0) {
        return {
            x: 10,
            y: 10,
        };
    } else if (key === 1) {
        return {
            ...defaultMove,
            y: defaultMove.y + 50,
        };
    }
    return defaultMove;
}