Binding prop trong Vue Typescript
Xin chào mọi người Mình đang học vue và đang làm project cơ bản để học nó Mình dùng vue 3 và typescript Mình đang có một component MenuItem có type cơ bản như sau
interface MenuItemProps {
title: string;
to?: string | '';
icon?: IconName | '';
}
withDefaults(defineProps<MenuItemProps>(), {
to: '',
icon: '',
});
1 prop required và 2 prop optional => Ở đây mình muốn vừa là link vừa là item normal cho các action như show modal và change language.
Ở component Menu mình gọi nó như sau
export const BASE_MENU: BaseMenu[] = [
{
icon: 'language',
title: 'Language',
languages: languages.map((lang) => ({
title: lang.title,
lang: lang.lang,
})),
},
{
icon: 'help',
title: 'Help',
},
];
interface MenuItem {
title: string;
icon: IconName;
}
export interface MenuItemLink extends MenuItem {
to: string;
}
export interface MenuItemLangue extends Omit<MenuItem, 'icon'> {
languages?: {
title: string;
lang: string;
}[];
}
export type BaseMenu = MenuItemLangue & Partial<Pick<MenuItemLink, 'to' | 'icon'>>;
type Menu = BaseMenu[] | MenuItemLangue;
const menuItems = reactive<{ data: Menu }>({ data: props.items });
const handleChange = (item: BaseMenu, _evt: Event) => {
if (item.icon === 'language') {
menuItems.data = item;
}
...
};
...
<MenuItem
v-for="(item, index) in currentMenuList"
:key="index"
:to="item.to"
:icon="item.icon"
@onClick="(event) => handleChange(item, event)"
/>
Ở đây mình muốn là là khi bấm vào cái menu item language thì render list languages.
Ở đây thì mỗi item trong currentMenuList nó sẽ có 2 type là
{ languages?: { title: string; lang: string; }[] | undefined; title: string; to?: string | undefined; icon?: IconName | undefined; } | { title: string; lang: string; }
Lúc này mình passing :to="item.to" :icon="item.icon"
thì typescript lỗi là Property 'icon' does not exist on type '{ title: string; lang: string; }'
Lúc này mình search gg thử thì có thấy dùng v-bind.
Mình lại thử như này thì vẫn lỗi v-bind="{ to: item?.to || '', icon: item?.icon || '', title: item.title }"
Mình lại tiếp tục thử kiểu này.
<MenuItem
v-for="(item, index) in currentMenuList"
:key="index"
v-bind="item"
@onClick="(event) => handleChange(item, event)"
/>
Vâng thế này thì nó hết lỗi. Nhưng có vẽ không hay lắm, khó biết được props mình passing.
Mọi người có cách nào binding mà không dính lỗi type không ạ
1 CÂU TRẢ LỜI
Theo mình thì có thể thiết kế như sau:
// trong file chứa BASE_MENU,
// bạn khai báo interface MenuItem generic hết mức có thể
// để cover cả 2 loại link item và language item
export interface MenuItem { // export hẳn interface này sang component MenuItem để reuse
title: string;
to?: string;
icon?: IconName;
data?: string; // "data" ở đây chính là "lang" của bạn, mình dùng tên "data" vì nó generic hơn "lang"
children?: MenuItem[];
}
export const BASE_MENU: MenuItem[] = [
{
icon: "language",
title: "Language",
children: languages.map((lang) => ({
title: lang.title,
data: lang.lang,
})),
},
{
icon: "help",
title: "Help",
},
];
// component MenuItem.vue
import { MenuItem } from "./baseMenu.ts";
interface MenuItemProps extends MenuItem;
Giờ việc reuse lại component MenuItem sẽ ko bị lỗi type, type của bạn cũng được reuse mà ko bị rối 🙂 Bạn thử xem được ko nhé.
Trước hết là cảm ơn anh với cách tiếp cận này . Dễ đọc cho mọi người.
Nhưng mà hiện tại nó lại bị lỗi như ri đây anh
// eslint-disable-next-line @typescript-eslint/no-empty-interface interface MenuItemProps extends BaseMenuItem {}
const props = withDefaults(defineProps<MenuItemProps>(), {}); Nếu mà như này sẽ lỗi
MenuItem.vue:22:26: ERROR: Unexpected "}"
const props = defineProps<MenuItemProps>();
Còn mà viết như này thì title = undefined
export interface BaseMenuItem {
title: string;
to?: string;
icon?: IconName;
children?: BaseMenuItem[];
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface MenuItemProps extends BaseMenuItem {}
const props = defineProps<MenuItemProps>();
Còn như này thì mọi thử oke @@
@hungify Cách 3 có vẻ là chính xác rồi đó bạn. Mình cũng là rookie TypeScript thôi nên cũng ko tránh sai sót 😅
Giải thích một tí cái lỗi eslint no-empty-interface
thì đây là một convention mặc định của TS để hạn chế việc khai báo các interface thừa thãi/vô nghĩa. Trong trường hợp này mình khai báo thêm interface MenuItemProps
extends BaseMenuItem
với mục đích là tách biệt interface dùng cho component với interface dùng cho menu item, cover những trường hợp có thể component sẽ chứa những property khác nữa, nhưng vì chưa biết sẽ có gì nên hiện tại 2 interface hoàn toàn giống nhau => eslint báo lỗi trên.
Bạn có thể comment ignore như cách bạn đang làm hoặc bỏ hẳn MenuItemProps
và dùng BaseMenuItem
luôn:
const props = defineProps<BaseMenuItem>();
@khangnd
Nhưng mà viết như này sẽ lỗi nha anh const props = defineProps<BaseMenuItem>();vue
Cái này em quên note ở comment trên
Note: https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
Theo mình thì cách thiết kế types của bạn đang bị phức tạp hóa, mình chưa có solution cụ thể cho bạn, nhưng thiết nghĩ nên tìm cách đơn giản lại sẽ tốt hơn cho code readability cũng như dễ debug sau này, mà nhất là có thể giải quyết luôn vấn đề bạn đang hỏi 🙂
@khangnd Vâng Em cũng thấy rối thiệt mà em muốn bấm vào đúng item nớ thì lấy key languages trong chính nó luôn
Kiểu như dưới hình á anh
Em muốn reuse lại component MenuItem ấy
Nhưng vấn để là cái MenuItem có các props typpe là optional.
Em chưa biết passing như nào cả