Frontend Development 18 min read

Enhancing Element Plus ElTable with Vue 3.3 Generic Components and defineSlots for Strong Type Safety

This article explains how to combine Vue 3.3 generic components and the new defineSlots feature to create a lightweight, column‑configurable wrapper around Element Plus's ElTable that provides full TypeScript type hints for row data and custom column slots, improving maintainability and developer experience.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Enhancing Element Plus ElTable with Vue 3.3 Generic Components and defineSlots for Strong Type Safety

The article introduces a second‑level encapsulation of Element Plus's ElTable that leverages Vue 3.3's generic components and defineSlots , two features that are rarely used together in real‑world projects.

It starts by describing the pain points of the default ElTable implementation, where dozens of el-table-column components must be written manually, making maintenance cumbersome. Other UI libraries such as Ant Design Vue, Naive UI, and TDesign solve this by using a simple columns configuration.

To illustrate a basic solution, the article defines TypeScript interfaces for the wrapper's props:

export interface IFsTableProps {
  data: any[];
  columns: IFsTableColumn[];
}

export interface IFsTableColumn {
  prop: string;
  label: string;
  [key: string]: any;
}

It then shows a minimal FsTable component that iterates the columns array to render el-table-column elements:

<template>
  <div class="fs-table-container">
    <el-table :data="props.data" :=" $attrs">
      <el-table-column v-for="item in props.columns" :key="item.prop" :="item"></el-table-column>
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { IFsTableProps } from "./type";
const props = defineProps<IFsTableProps>();
</script>

<style scoped>
.fs-table-container { width: 100%; height: 100%; }
</style>

A parent component uses the wrapper by passing a columns array and a tableData array:

<template>
  <div class="container">
    <fs-table :data="tableData" :columns="columns" style="width: 100%"></fs-table>
  </div>
</template>

<script setup lang="ts">
import FsTable from "./components/FsTable/FsTable.vue";
import { IFsTableColumn } from "./components/FsTable/type";
const columns: IFsTableColumn[] = [
  { prop: "date", label: "Date", width: "180" },
  { prop: "name", label: "Name", width: "180" },
  { prop: "address", label: "Address" }
];
const tableData = [
  { date: "2016-05-03", name: "Tom", address: "No. 189, Grove St, Los Angeles" },
  // ...more rows
];
</script>

<style scoped>
.container { width: 100vw; height: 100vh; overflow: hidden; }
</style>

The main drawback of this approach is that both the wrapper and the original ElTable type the row data as any , so developers lose IntelliSense when accessing row.xxx fields.

To solve this, the article proposes turning the wrapper into a generic component. By adding a generic type parameter T to the script setup tag ( generic="T" ) and defining props as data: T[] , the component can infer the exact row type from the parent.

Type constraints can be added (e.g., T extends { id: number } ) to enforce required fields and provide compile‑time errors when the parent supplies incompatible data.

Next, the article introduces defineSlots to give slots strong type definitions. A simple example defines a default slot that must receive a test: string prop, causing the compiler to warn if the slot is used without that prop.

Combining generics with slots, the article solves the missing type hints for column slots. It defines a unified bodyCell slot that receives both the row data and a columnKey identifier:

defineSlots<{
  bodyCell(props: { row: T; columnKey: string }): any;
}>();

The column interface is extended with a new columnKey field:

export interface IFsTableColumn {
  prop: string;
  label: string;
  columnKey: string; // new
  [key: string]: any;
}

The component template then renders each column and forwards the row and columnKey to the bodyCell slot:

<template>
  <div class="fs-table-container">
    <el-table :data="props.data" :=" $attrs">
      <el-table-column v-for="item in props.columns" :key="item.prop" :="item">
        <template #default="scope">
          <slot name="bodyCell" :row="scope.row" :columnKey="item.columnKey">{{ scope.row[item.prop] }}</slot>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

In the parent component, developers can now customize any column by checking columnKey inside the bodyCell slot and receive full type information for row :

<template>
  <div class="container">
    <fs-table :data="tableData" :columns="columns" style="width: 100%">
      <template #bodyCell="{ row, columnKey }">
        <template v-if="columnKey === 'name'">
          <div style="color: red; font-size: 25px">{{ row.name }}</div>
        </template>
      </template>
    </fs-table>
  </div>
</template>

With this setup, the IDE now offers proper autocomplete for all fields of the row object, eliminating the need to remember field names manually and dramatically improving developer productivity.

The article concludes by providing a link to the full source code on GitHub and notes that while the presented wrapper is minimal, more comprehensive implementations can be found in larger open‑source projects.

TypeScriptVueSlotsElTableGeneric Component
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.