Cuando crea componentes para su proyecto, todo comienza divertido y fácil. Añade un poco de estilo y ya está. MyButton.vue <template> <button class="my-fancy-style"> <slot></slot> </button> </template> Entonces te das cuenta de inmediato de que necesitas una docena de props, porque tu equipo de diseño quiere que sea de diferentes colores y tamaños, con iconos a la izquierda y a la derecha, con contadores... 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 }); Después de todo, no puedes tener los botones “Cancelar” y “Ok” del mismo color, y los necesitas para reaccionar a las interacciones de los usuarios. 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 }); Bueno, tienes la idea: habrá algo salvaje, como pasar o agregando autofocus - todos sabemos cómo es simple en Figma hasta que la vida real golpee duro. width: 100% Ahora imagina un botón de enlace: parece el mismo, pero cuando lo presiona, debe ir al enlace externo o interno. o etiquetas cada vez, pero por favor no. También puede agregar y Propiedades a su componente inicial, pero se sentirá asfixiado muy pronto: <RouterLink> <a> to href <component :is="to ? RouterLink : href ? 'a' : 'button'" <!-- ugh! --> Por supuesto, necesitará un componente de "segundo nivel" que envuelva su botón (también manejará los enlaces hipervínculos predeterminados y algunas otras cosas interesantes, pero los omitiré por motivos de simplicidad): <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> Y aquí es donde comienza nuestra historia. Square One Plaza Uno Bueno, básicamente va a funcionar, no voy a mentir. Pero no habrá autocomplete para los complementos derivados, lo que no es cool: <MyLinkButton :counter=“2"> Podemos propagar props en silencio, pero el IDE no sabe nada sobre ellos, y eso es una vergüenza. La solución simple y obvia es propagarlos explícitamente: <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> El IDE tendrá el autocomplete adecuado.Tendremos mucho dolor y lamento apoyarlo. Obviamente, el principio de “No se repita a sí mismo” no se aplicó aquí, lo que significa que tendremos que sincronizar cada actualización. Un día, tendrás que añadir otro prop, y tendrás que encontrar cada componente que envuelva el básico. Sí, el botón y LinkButton son probablemente suficientes, pero imagina TextInput y una docena de componentes que dependen de él: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput. Después de todo, es feo.Y cuanto más props tenemos, más feo se vuelve. Clean It Up Limpia lo de arriba Es bastante difícil reutilizar un tipo anónimo, por lo que le damos un nombre. // MyButton.props.ts export interface MyButtonProps { theme?: ComponentTheme; small?: boolean; icon?: IconSvg; rightIcon?: IconSvg; counter?: number; disabled?: boolean; loading?: boolean; } No podemos exportar una interfaz de la archivo debido a algún interno magia, por lo que necesitamos crear una En el lado brillante, mira lo que tenemos aquí: .vue script setup .ts const props = withDefaults(defineProps<MyButtonProps>(), { theme: ComponentTheme.BLUE, icon: undefined, rightIcon: undefined, counter: undefined, }); Mucho más limpio, ¿no es? y aquí está el heredado: interface MyLinkButtonProps { to?: RouteLocationRaw; href?: string; } const props = defineProps<MyButtonProps & MyLinkButtonProps>(); Sin embargo, aquí hay un problema: ahora, cuando los complementos básicos se tratan como de los propósitos, no se propagan con Ya no, así que tenemos que hacerlo nosotros mismos. 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> Es todo bueno, pero pasamos un poco más de lo que queremos: Como podéis ver, ahora nuestro botón subyacente también tiene un No es una tragedia, solo un poco de desorden y bytes adicionales, aunque no sea cool. 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> Ahora, solo pasamos lo que tiene que pasar, pero todas esas letras de cuerdas no parecen increíbles, ¿no? y esa es la historia más triste de TypeScript, chicos. Interfaces vs Abstract Interfaces Interfaces versus interfaces abstractas Si alguna vez ha trabajado con los idiomas adecuados orientados a objetos, probablemente conozca cosas como: Desafortunadamente, en TypeScript, las interfaces son efímeras; no existen en tiempo de ejecución, y no podemos averiguar fácilmente qué campos pertenecen a la . Reflexiones MyButtonProps Esto significa que tenemos dos opciones.En primer lugar, podemos mantener las cosas como son: cada vez que añadimos un También hay que excluirlo de (Y incluso si lo olvidamos, no es una gran cosa). MyLinkButton propsToPass La segunda manera es usar objetos en lugar de interfaces. Puede sonar sin sentido, pero déjame codificar algo: no será horrible; prometo. horrorífico . que // MyButton.props.ts export const defaultMyButtonProps: MyButtonProps = { theme: ComponentTheme.BLUE, small: false, icon: undefined, rightIcon: undefined, counter: undefined, disabled: false, loading: false, }; No tiene sentido crear un objeto sólo para crear un objeto, pero podemos usarlo para los props predeterminados. Se vuelve más limpio: MyButton.vue const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps); Ahora solo tenemos que actualizar en : propsToPass MyLinkButton.vue const propsToPass = computed(() => Object.fromEntries( Object.entries(props).filter(([key, _]) => Object.hasOwn(defaultMyButtonProps, key) ) ) ); Para realizar este trabajo, necesitamos definir explícitamente todos los y campos en De lo contrario, el objeto no “tiene su propio”. undefined null defaultMyButtonProps De esta manera, cada vez que añadas un propósito al componente básico, también tendrás que añadirlo al objeto con valores predeterminados. Así que sí, hay dos lugares de nuevo, y tal vez no sea mejor que la solución del capítulo anterior. I’m Done Estoy hecho No es una obra maestra, pero es probablemente lo mejor que podemos hacer dentro de las limitaciones de TypeScript. También parece que tener tipos de prop dentro del archivo SFC es mejor, pero no puedo decir que moverlos a un archivo separado lo hizo mucho peor. Puedes encontrar el código de este artículo en GitHub. GitHub