vue3 父元件給子元件傳遞泛型(不用JSX)

老貓抽旱菸發表於2023-05-06

最近在封裝一個元件,使用的時候希望父元件能給子元件傳遞一個泛型,在網上搜了半天,答案都是說要用jsx才能實現。具體寫法如下:

使用JSX

這段程式碼來自Eluxjs的示例專案elux-vue-antd-admin,感興趣的可以看下。
父元件: (為減少篇幅,程式碼刪了很多,當虛擬碼看吧)

const Component = defineComponent<Props>({
  props: ['listPathname', 'mergeColumns'] as any,
  setup(props) {
    return () => {
      return (
        <MTable<ListItem> // 這裡使用子元件MTable,傳遞泛型ListItem
          size={tableSize}
          commonActions={commonActions}
          selection={selection}
          loading={listLoading === 'Start' || listLoading === 'Depth'}
        />
      );
    };
  },
});

export default Component;

子元件:(子元件比較麻煩)

interface Props<T = Record<string, any>> extends TableProps<T> {
  class?: string;
  columns: MColumns<T>[];
  selectedRows?: Partial<T>[];
  selection?: MSelection<T>;
}

const Component = defineComponent<Props>({
  name: styles.root,
  props: [
    'class',
    'columns'
  ] as any,
  setup(props: Props) {
    return () => {
      const {class: className = '', dataSource, onChange, rowKey = 'id', loading, size, bordered, locale, scroll} = props;
      return (
        <div class={styles.root + ' ' + className}>
          {headArea.value}
          <Table
            columns={columnList.value}
            rowSelection={rowSelection.value}
            onChange={onChange}
            size={size}
          />
        </div>
      );
    };
  },
}) as any;

export default Component as <T>(props: Props<T>) => JSX.Element;

這裡使用 Component as <T>(props: Props<T>) => JSX.Element; 來做到接收泛型,但消費它的父元件必須用 jsx。
因為專案不方便用jsx,我找了很久如何用template來傳遞泛型,終於找到了!

使用Template

先看子元件:

<script
  lang="ts"
  setup
  // 關鍵:setup script有個generic屬性,可以宣告泛型,供元件使用
  generic="T extends { name: string; age: number; }, Q extends { title:string }"
>
// 在props中使用泛型,rowKey使用keyof T,plainObj使用第二個泛型Q
defineProps<{
  personList: T[];
  rowKey: keyof T;
  plainObj: Q;
}>();
</script>

<template>
  <div>
    // 模板中正常使用props
    <h3 v-for="item in personList" :key="item.name">
      {{ item.age }}
    </h3>
    // 用插槽向父元件暴露資料,plainObj的型別是Q
    <slot :obj="plainObj" />
  </div>
</template>

generic屬性的作用就是給元件中使用的泛型提供一個“來源”,這樣父元件消費子元件的時候,只要給personList傳遞一個資料,泛型T就會被自動推斷出來。

generic屬性的泛型可以選擇extends一些已知的屬性,因為子元件自身使用到props的哪些屬性,和消費該元件的地方是無關的。

父元件:

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue';
// 給list的item宣告型別,該型別可以是子元件T的超集
interface DataType {
  name: string;
  age: number;
  gender: 'male' | 'female' | 'wallmart bag';
  hobby?: 'jk' | 'dk' | '敵法師';
}
const list: DataType[] = [
  {
    name: 'ming',
    age: 20,
    gender: 'male',
    hobby: '敵法師'
  },
  {
    name: 'hong',
    age: 20,
    gender: 'female'
  },
  {
    name: 'hua',
    age: 20,
    gender: 'wallmart bag'
  }
];
</script>

<template>
  <div>
    <HelloWorld
      :person-list="list"
      row-key="genderr"
      :plainObj="{ happy: true, title: '抬頭' }"
      v-slot="{ obj }"
    >
      <!-- ? 不能將型別“"agg"”分配給型別“keyof T”。 -->

      {{ obj.happ }}
      <!-- ? 屬性“happ”在型別“{ happy: boolean; title: string; }”上不存在。你是否指的是“happy”? -->
    </HelloWorld>
  </div>
</template>

<style></style>

可以看到,子元件推斷出了T的型別是DataType,報錯 型別"genderr"不可分配給型別“keyof T”。你的意思是"gender"?
Q也成功的推斷了出來,報錯 屬性“happ”在型別“{ happy: boolean; title: string; }”上不存在,這裡的 { happy: boolean; title: string; } 即是我們傳給 plainObj 屬性的泛型Q

這樣就實現了父元件透過props,給子元件傳遞所需的泛型

generic屬性的泛型如何extends一些外部的型別,或者extends已有的介面呢?
額外使用一個script標籤即可

<script>
// 額外使用一個script標籤即可
import { SecendType } from "./index";
interface FirstType {
  name: string;
  age: number;
}
</script>

<script lang="ts" setup generic="T extends FirstType, Q extends SecendType">
defineProps<{
  personList: T[];
  rowKey: keyof T;
  plainObj: Q;
}>();
</script>

<template>
  <div>
    <h3 v-for="item in personList" :key="item.name">
      {{ item.age }}
    </h3>
    <slot :obj="plainObj" />
  </div>
</template>

相關文章