Boolean - легкий способ сделать плохой API
Что? Почему? Это же прекрасный тип данных - всего два значения, которые так легко использовать...
Проще всего показать проблему на примере. Представьте, что мы делаем обертку над webpack, которая прячет в себя весь ужас вебпака и дает такой простой програмный API для использования:
function buildApp(entryPoint: string): Promise<void>;
Вроде бы всё просто и понятно. Чуть позже вы понимаете, что части ваших пользователей хочется немного настраивать вашу сборку - они хотят использовать ts-loader вместо babel-loader для обработки ts файлов.
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' - вдруг вы захотите менять его поведение в зависимости от количества опций?
Вообщем проектируйте апи правильно, думайте не только о том как удобно написать вам сейчас, но и как оно будет работать потом, и как это будут использовать.