September 7, 2024

Boolean - легкий способ сделать плохой API

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

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

function buildApp(entryPoint: string): Promise<void>;

Вроде бы всё просто и понятно. Чуть позже вы понимаете, что части ваших пользователей хочется немного настраивать вашу сборку - они хотят использовать ts-loader вместо babel-loader для обработки ts файлов.

Что-ж, доработаем наш API!

function buildApp(
  entryPoint: string,
  useTsLoader: boolean = false,
): Promise<void>;

При вызове это выглядит как то так:

await buildApp('./my-entry.tsx', true);

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

type BuildAppParams = {
  entryPoint: string;
  useTsLoader?: boolean;
}
function buildApp({
  entryPoint,
  useTsLoader = false,
}: BuildAppParams): Promise<void>;

// вызов
buildApp({
  entryPoint: './my-entry.tsx',
  useTsLoader: true,
});

Красота? Вроде бы все понятно?

Нет. А что если мы напишем useTsLoader: false? Что это вообще будет означать? Что будет использоваться вместо него? Конечно можно прочитать документацию и понять какое будет поведение..

А что если через какое то время мы захотим добавить еще немного гибкости, и давать возможность использовать например swc или esbuild?

type BuildAppParams = {
  entryPoint: string;
  useTsLoader?: boolean;
  useSwcLoader?: boolean;
};
function buildApp(options: BuildAppParams): Promise<void>;

Теперь вопрос - что будет происходить когда и useTsLoader, и useSwcLoader выставлены в true? Наверное можно указать в документации что использование параметров одновременно - некорректно, или даже попробовать починить это на уровне типов:

type BuildAppPrams = {
  entryPoint: string;
} & ({
  useTsLoader: true;
  useSwcLoader?: false;
} | {
  useTsLoader?: false;
  useSwcLoader: true;
} | {
  useTsLoader?: false;
  useSwcLoader?: false;
})

// валидно:
buildApp({
  entryPoint: './my-entry.tsx',
});
buildApp({
  entryPoint: './my-entry.tsx', useTsLoader: true,
});
buildApp({
  entryPoint: './my-entry.tsx', useSwcLoader: true,
});
// ошибка от ts:
buildApp({
  entryPoint: './my-entry.tsx',
  useSwcLoader: true,
  useTsLoader: true,
});

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

Что же делать?

Очевидно, использовать перечисляемые типы! enum в ts недолюбливают, но есть понятная альтернатива - union + literal types.

type BuildAppParams = {
  entryPoint: string;
  loader?: 'ts' | 'babel' | 'swc';
};

buildApp({ entryPoint: './my-entry.tsx', loader: 'ts' });

Этот вариант стоит использовать даже если на текущий момент у вас предполагается всего два варианта - кто знает как захочется расширить API в будущем?

Другой вариант, предлагаемый Мартином Фаулером - делать полность отдельные методы вместо использования флагов:

function buildAppWithBabel(entryPoint: string): Promise<void>;
function buildAppWithTs(entryPoint: string): Promise<void>;
function buildAppWithSwc(entryPoint: string): Promise<void>;

Мне персонально такой вариант в мире react кажется менее удобным, но полноценных аргументов кроме вкусовщины на это у меня нет.

Конечно, бывают варианты, когда boolean является очевидным и правильным решением. Условный setModalOpen(boolean) будет понятен, и вариантов с наполовину открытой модалкой у вас скорее всего не появится. Но даже в подобных случаях лучше задумываться - например проп для селекта defaultOpen возможно стоит заменить на defaultState: 'open' | 'closed' - вдруг вы захотите менять его поведение в зависимости от количества опций?

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