Коли ви створюєте компоненти для свого проекту, все починається весело і легко. Додайте трохи стилю, і ось що. MyButton.vue <template> <button class="my-fancy-style"> <slot></slot> </button> </template> Тоді ви відразу ж усвідомлюєте, що вам потрібно десяток пристосувань, тому що ваша команда дизайнерів хоче, щоб він був різних кольорів і розмірів, з іконами зліва і праворуч, з лічильниками... const props = withDefaults(defineProps<{ theme?: ComponentTheme; small?: boolean; icon?: IconSvg; // I’ve described how I cook icons in my previous article rightIcon?: IconSvg; counter?: number; }>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined }); Зрештою, ви не можете мати кнопки «Відмінити» і «Ок» одного кольору, і вам потрібно, щоб вони реагували на взаємодії користувачів. const props = withDefaults(defineProps<{ theme?: ComponentTheme; small?: boolean; icon?: IconSvg; rightIcon?: IconSvg; counter?: number; disabled?: boolean; loading?: boolean; }>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined }); Ну, ви отримуєте ідею: буде щось дике, як проходження або додаючи автофокус - ми всі знаємо, як це виглядає просто в Figma, поки реальне життя не вдарить важко. width: 100% Тепер уявіть кнопку посилання: вона виглядає однаково, але коли ви натискаєте її, ви повинні перейти до зовнішнього або внутрішнього посилання. або щоразу, але будь ласка, не. Ви також можете додати та Пропозиції до вашого початкового компонента, але ви будете відчувати задихання досить скоро: <RouterLink> <a> to href <component :is="to ? RouterLink : href ? 'a' : 'button'" <!-- ugh! --> Звичайно, вам знадобиться компонент "другого рівня", який обертає вашу кнопку (він також буде справлятися з умовними обрисами гіперпосилання та деякими іншими цікавими речами, але я пропустив їх заради простоти): <template> <component :is="props.to ? RouterLink : 'a'" :to="props.to" :href="props.href" class="my-link-button" > <MyButton v-bind="$attrs"> <slot></slot> </MyButton> </component> </template> <script lang="ts" setup> import MyButton from './MyButton.vue'; import { RouteLocationRaw, RouterLink } from 'vue-router'; const props = defineProps<{ to?: RouteLocationRaw; href?: string; }>(); </script> І ось тут починається наша історія. Square One Площа 1 Ну, в основному це буде працювати, я не буду брехати. ви все ще можете ввести Але не буде автокомплекту для похідних пропів, що не круто: <MyLinkButton :counter=“2"> Ми можемо розповсюджувати прописки тихо, але IDE нічого про них не знає, і це ганьба. Просте і очевидно рішення полягає в тому, щоб розповсюджувати їх прямо: <template> <component :is="props.to ? RouterLink : 'a'" :to="props.to" :href="props.href" class="my-link-button" > <MyButton :theme="props.theme" :small="props.small" :icon="props.icon" :right-icon="props.rightIcon" :counter="props.counter" :disabled="props.disabled" :loading="props.loading" > <slot></slot> </MyButton> </component> </template> <script lang="ts" setup> // imports... const props = withDefaults( defineProps<{ theme?: ComponentTheme; small?: boolean; icon?: IconSvg; rightIcon?: IconSvg; counter?: number; disabled?: boolean; loading?: boolean; to?: RouteLocationRaw; href?: string; }>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined, } ); </script> Це буде працювати. ІДЕ буде мати належний автокомплект. У нас буде багато болю і шкода підтримувати його. Очевидно, що принцип «Не повторюйте себе» тут не був застосований, а це означає, що нам доведеться синхронізувати кожне оновлення. Одного дня вам доведеться додати ще один проп, і вам доведеться знайти кожен компонент, який обертає основний. Так, Кнопка і LinkButton, ймовірно, достатньо, але уявіть TextInput і десяток компонентів, які залежать від нього: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput. Адже це погано.І чим більше пристосувань у нас є, тим гірше воно стає. Clean It Up Очистити його вгору Дуже складно повторно використовувати анонімний тип, тому давайте назвемо його. // MyButton.props.ts export interface MyButtonProps { theme?: ComponentTheme; small?: boolean; icon?: IconSvg; rightIcon?: IconSvg; counter?: number; disabled?: boolean; loading?: boolean; } Ми не можемо експортувати інтерфейс з Файли через внутрішню магії, тому ми повинні створити окрему На світлій стороні, дивіться, що ми маємо тут: .vue script setup .ts const props = withDefaults(defineProps<MyButtonProps>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined, }); Чистіше, чи не так? і ось успадкований один: interface MyLinkButtonProps { to?: RouteLocationRaw; href?: string; } const props = defineProps<MyButtonProps & MyLinkButtonProps>(); Однак, ось проблема: тепер, коли базові прописки розглядаються як «Перспективи, які не розповсюджуються Більше того, ми повинні це зробити самі. MyLinkButton v-bind=”$attrs” <!-- MyLinkButton.vue --> <component :is="props.to ? RouterLink : 'a'" :to="props.to" :href="props.href" class="my-link-button" > <MyButton v-bind="props"> <!-- there we go --> <slot></slot> </MyButton> </component> Все добре, але ми передаємо трохи більше, ніж хочемо: Як бачите, тепер наша підземна кнопка також має Це не трагедія, просто трохи плутанини і додаткових байтів, хоча і не круто. href <template> <component :is="props.to ? RouterLink : 'a'" :to="props.to" :href="props.href" class="my-link-button" > <MyButton v-bind="propsToPass"> <slot></slot> </MyButton> </component> </template> <script lang="ts" setup> // imports and definitions… const props = defineProps<MyButtonProps & MyLinkButtonProps>(); const propsToPass = computed(() => Object.fromEntries( Object.entries(props).filter(([key, _]) => !["to", "href"].includes(key)) ) ); </script> Тепер ми тільки передаємо те, що має бути передано, але всі ці строкові літератури не виглядають чудово, чи не так? Interfaces vs Abstract Interfaces Інтерфейси проти абстрактних інтерфейсів Якщо ви коли-небудь працювали з відповідними об'єктно-орієнтованими мовами, ви, напевно, знаєте про такі речі, як: На жаль, в TypeScript інтерфейси є ефемеральними; вони не існують під час роботи, і ми не можемо легко з'ясувати, які поля належать до . рефлексії MyButtonProps Це означає, що у нас є два варіанти.По-перше, ми можемо тримати речі такими, якими вони є: кожного разу, коли ми Також необхідно виключити з (І навіть якщо ми забудемо про це, це не велика справа). MyLinkButton propsToPass Другий спосіб - використовувати об'єкти замість інтерфейсів. Це може звучати безглуздо, але дозвольте мені щось кодувати: це не буде жахливо; я обіцяю. Страшним є Цей // MyButton.props.ts export const defaultMyButtonProps: MyButtonProps = { theme: ComponentTheme.BLUE, small: false, icon: undefined, rightIcon: undefined, counter: undefined, disabled: false, loading: false, }; Немає сенсу створювати об'єкт тільки для створення об'єкта, але ми можемо використовувати його для за замовчуванням. Вони стають чистішими: MyButton.vue const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps); Потрібно лише оновити в : propsToPass MyLinkButton.vue const propsToPass = computed(() => Object.fromEntries( Object.entries(props).filter(([key, _]) => Object.hasOwn(defaultMyButtonProps, key) ) ) ); Для цього необхідно чітко визначити всі та Поля в В іншому випадку об’єкт не «власний». undefined null defaultMyButtonProps Таким чином, коли ви додаєте прописку до базового компонента, вам також доведеться додати її до об'єкта з умовними значеннями. Так, так, це два місця знову, і, можливо, це не краще, ніж рішення з попередньої глави. I’m Done Я зробив Це не шедевр, але це, мабуть, найкраще, що ми можемо зробити в межах обмежень TypeScript. Також здається, що мати типи пропів всередині файлу SFC краще, але я не можу сказати, що переміщення їх в окремий файл зробило це набагато гірше. Код з цієї статті можна знайти на GitHub. GitHub