När du skapar komponenter för ditt projekt börjar allt roligt och enkelt. Lägg till lite styling och så är det. MyButton.vue <template> <button class="my-fancy-style"> <slot></slot> </button> </template> Då inser du omedelbart att du behöver ett dussin props, eftersom ditt designteam vill att det ska vara av olika färger och storlekar, med ikoner på vänster och höger, med räknare ... 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 }); Det är fortfarande okej; det är meningsfullt.När allt kommer omkring kan du inte ha "Avbryt" och "Ok" knappar av samma färg, och du behöver dem för att reagera på användarinteraktioner. 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 }); Tja, du får idén: det kommer att finnas något vilt, som att passera eller lägga till autofokus – vi vet alla hur enkelt det ser ut i Figma tills det verkliga livet slår hårt. width: 100% Nu föreställ dig en länkknapp: den ser likadan ut, men när du trycker på den bör du gå till den externa eller interna länken. eller taggar varje gång, men snälla inte. du kan också lägga till och Props till din ursprungliga komponent, men du kommer att känna kvävning ganska snart: <RouterLink> <a> to href <component :is="to ? RouterLink : href ? 'a' : 'button'" <!-- ugh! --> Naturligtvis behöver du en "andra nivån" -komponent som omsluter din knapp (det kommer också att hantera standard hyperlänkar och några andra intressanta saker, men jag kommer att utelämna dem för enkelhetens skull): <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> Och det är där vår historia börjar. Square One torget ett Tja, i grund och botten kommer det att fungera, jag kommer inte att ljuga. du kan fortfarande skriva Men det kommer inte att finnas någon autokomplett för de härledda propparna, vilket inte är coolt: <MyLinkButton :counter=“2"> Vi kan sprida props tyst, men IDE vet inte något om dem, och det är synd. Den enkla och uppenbara lösningen är att sprida dem uttryckligen: <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> Det kommer att fungera. IDE kommer att ha rätt autokomplete. Vi kommer att ha mycket smärta och ångra att stödja det. Självklart tillämpas inte principen "Gör inte upprepning" här, vilket innebär att vi måste synkronisera varje uppdatering. En dag måste du lägga till en annan prop, och du måste hitta varje komponent som omsluter den grundläggande. Ja, Knapp och LinkButton är förmodligen tillräckligt, men föreställ dig TextInput och ett dussin komponenter som är beroende av det: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput. När allt kommer omkring är det fult. Och ju fler props vi har, desto fult blir det. Clean It Up Städa upp det Det är ganska svårt att återanvända en anonym typ, så låt oss ge den ett namn. // MyButton.props.ts export interface MyButtonProps { theme?: ComponentTheme; small?: boolean; icon?: IconSvg; rightIcon?: IconSvg; counter?: number; disabled?: boolean; loading?: boolean; } Vi kan inte exportera ett gränssnitt från filen på grund av vissa interna magiska, så vi måste skapa en separat På den ljusa sidan, se vad vi har här: .vue script setup .ts const props = withDefaults(defineProps<MyButtonProps>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined, }); Mycket renare, är det inte? och här är den ärvda: interface MyLinkButtonProps { to?: RouteLocationRaw; href?: string; } const props = defineProps<MyButtonProps & MyLinkButtonProps>(); Men här är ett problem: nu, när grundläggande proppar behandlas som ”s props, de sprids inte med längre, så vi måste göra det själva. 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> Det är allt bra, men vi passerar lite mer än vi vill: Som ni kan se, nu har vår underliggande knapp också en Det är inte en tragedi, bara lite röran och extra bytes, även om det inte är coolt. 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> Nu passerar vi bara det som måste passeras, men alla de strängbokstäverna ser inte fantastiska ut, gör de? Interfaces vs Abstract Interfaces Gränssnitt vs abstrakta gränssnitt Om du någonsin har arbetat med lämpliga objektorienterade språk vet du förmodligen om sådana saker som Tyvärr, i TypeScript, gränssnitt är ephemeral; de existerar inte vid körtid, och vi kan inte lätt ta reda på vilka fält som tillhör . Reflektioner MyButtonProps Det betyder att vi har två alternativ. För det första kan vi hålla saker och ting som de är: varje gång vi lägger till en Vi måste också utesluta den från (Och även om vi glömmer bort det, är det inte en stor sak). MyLinkButton propsToPass Det kan låta meningslöst, men låt mig koda något: det kommer inte att vara hemskt, jag lovar. förskräckligt . Det där // MyButton.props.ts export const defaultMyButtonProps: MyButtonProps = { theme: ComponentTheme.BLUE, small: false, icon: undefined, rightIcon: undefined, counter: undefined, disabled: false, loading: false, }; Det är inte meningsfullt att skapa ett objekt bara för att skapa ett objekt, men vi kan använda det för standardprops. Det blir ännu renare: MyButton.vue const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps); Nu behöver vi bara uppdatera i : propsToPass MyLinkButton.vue const propsToPass = computed(() => Object.fromEntries( Object.entries(props).filter(([key, _]) => Object.hasOwn(defaultMyButtonProps, key) ) ) ); För att göra detta arbete måste vi uttryckligen definiera alla och fält i Annars ”har” objektet inte sitt eget. undefined null defaultMyButtonProps På så sätt, när du lägger till en prop till den grundläggande komponenten, måste du också lägga till den till objektet med standardvärden. Så, ja, det är två platser igen, och kanske är det inte bättre än lösningen från föregående kapitel. I’m Done Jag är gjord Det är inte ett mästerverk, men det är förmodligen det bästa vi kan göra inom TypeScript begränsningar. Det verkar också som att ha prop typer inuti SFC-filen är bättre, men jag kan inte säga att flytta dem till en separat fil gjorde det mycket värre. Du kan hitta koden från den här artikeln på GitHub. GitHub