iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

TypeScript 魔法 - 喚醒你的程式碼靈感系列 第 29

Day29 - 當 TypeScript 與 Vue Composition API 尬在一起

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20231014/20152047w31Ah1yFvT.png

深入了解 TypeScript 後,讓我們將其與 Vue Composition API 結合使用。有助於我們構建更具可維護性和型別安全性的 Vue 元件,降低錯誤風險,提高開發效率。透過範例,我們將更好的了解如何在 Vue3 中運用 TypeScript 和 Composition API。那麼,我們開始吧!

ref() 標記型別

初始值推斷型別

import { ref } from 'vue';

const name= ref('肉鬆'); // 推斷出的型別:Ref<string>

name.value = 2023 // TypeScript 報錯,Type 'number' is not assignable to type 'string'

Ref 型別

ref 內的值指定一個更複雜的型別,可以使用 Ref 型別:

import { ref } from 'vue';
import type { Ref } from 'vue';

const name: Ref<string | number> = ref('肉鬆');

name.value = 2023; // 通過

泛型參數覆蓋默認的推斷行為

import { ref } from 'vue';

const name = ref<string | number>('肉鬆');

name.value = 2023; // 通過

泛型參數未給初始值

當我們泛型參數未給初始值,會得到包含 undefined 的聯合型別:

import { ref } from 'vue';

const name = ref<string | number>(); // 推斷得到的型別:Ref<number | undefined>

reactive() 標記型別

import { reactive } from 'vue';

const theme = reactive({ title: 'TypeScript' }) // 推斷得到的型別:{ title: string }

使用介面明確標記 reactive 變數型別

import { reactive } from 'vue';

interface Theme {
  title: string;
}

const theme: Theme = reactive({ title: 'TypeScript' });

需要注意的是,不推薦使用 reactive() 的泛型參數,因為處理深層 ref 解包的返回值與泛型參數的型別可能不同。

import { reactive } from 'vue';

interface Person {
  age: ref<number>;
  name: string;
}

// 不推薦 reactive 使用泛型參數
const person = reactive<Person>({
  age: ref(30), // 此時 age 型別不是 number,而是 Ref<number>
  name: '肉鬆',
});

元件 props 標記型別

使用 <script setup>

當我們使用 <script setup> 時,defineProps() 宏函式(Macro Functions)支援參數中推斷型別,意味著可以自動推斷 props 的型別:

<script setup lang="ts">
const props = defineProps({
  name: {
    type: String,
    required: true,
  },
  age: Number,
});

props.name; // string
props.age; // number | undefined
</script>

這被稱之為「執行時宣告」,因為傳遞給 defineProps() 的參數會作為執行時的 props 選項使用。

然而,通過泛型參數來定義 props 的型別通常更直接:

<script setup lang="ts">
const props = defineProps<{
  name: string;
  age?: number;
}>();
</script>

這被稱之為「基於型別的宣告」。編譯器會盡可能的嘗試根據型別參數推斷出等價的執行時選項。在這種場景下,我們第二個例子中編譯出的執行時選項和第一個是完全一致的。基於型別的宣告或者執行時宣告可以擇一使用,但是不能同時使用。

我們也可以將 props 的型別移入一個單獨的介面中:

<script setup lang="ts">
interface Props {
  name: string;
  age?: number;
}

const props = defineProps<Props>();
</script>

語法限制

為了執行正確的程式碼,傳給 defineProps() 的泛型參數必須是以下之一:

  • 一個字面值型別
defineProps<{ /*... */ }>();
  • 同一個文件中的一個介面或物件字面值型別的引用
interface Props {/* ... */}

defineProps<Props>();

介面或物件字面值型別可以包含從其他文件導入的型別引用,但是,傳遞給 defineProps 的泛型參數本身不能是一個導入的型別:

import { Props } from './other-file'

defineProps<Props>(); // 不支援

這是因為 Vue 元件是單獨編譯的,編譯器目前不會抓取導入的文件以分析源型別。

Props 解構默認值

當使用基於型別的宣告時,我們失去了為 props 宣告默認值的能力。這可以通過 withDefaults 編譯器宏(Compiler Macros)解決:

export interface Props {
  name?: string;
  hobbies?: string[];
}

const props = withDefaults(defineProps<Props>(), {
  name: '肉鬆',
  hobbies: () => ['運動', '看書'],
});

這將被編譯為等效的運行時 props default 選項。此外,withDefaults 幫助程式為默認值提供型別檢查,並確保返回的props 型別刪除了已宣告默認值的屬性的可選標誌。

非使用 <script setup>

如果沒有使用 <script setup>,那麼為了開啟 props 的型別推斷,必須使用 defineComponent()。傳入 setup()props物件型別是從 props 選項中推斷而來。

import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    name: String,
  },
  setup(props) {
    props.name; // 型別:string
  },
});

元件 emits 標記型別

在 <script setup> 中,emit 函式的型別標記也可以通過執行時宣告或是型別宣告進行:

<script setup lang="ts">
// 執行時
const emit = defineEmits(['change', 'update']);

// 基於型別
const emit = defineEmits<{
  (e: 'change', id: number): void;
  (e: 'update', value: string): void;
}>();
</script>

這個型別參數應該是一個帶調用簽名的字面值型別。這個字面值型別的型別就是返回的 emit 函式的型別。我們可以看到,基於型別的宣告使我們可以對所觸發事件的型別進行更細微的控制。

若沒有使用 <script setup>defineComponent() 也可以根據 emits 選項自動推斷 setup 上下文中的 emit 函式的型別:

import { defineComponent } from 'vue';

export default defineComponent({
  emits: ['change'],
  setup(props, { emit }) {
    emit('change'); // 型別檢查 / 自動完成
  },
});

computed 標記型別

computed() 會自動從其計算函式的返回值上推斷出型別:

import { ref, computed } from "vue";

const count = ref(0);

// 推斷得到的型別:ComputedRef<number>
const double = computed(() => count.value * 2);

const result = double.value.split('');
// TypeScript 報錯: Property 'split' does not exist on type 'number'

使用泛型參數指定型別:

const double = computed<number>(() => {
  // 若返回值不是 number 型別則會報錯
})

事件處理函式標記型別

原生 DOM 事件

當我們在處理原生 DOM 事件時,應該為我們傳遞事件處理函式的參數標記型別:

<script setup lang="ts">
function handleChange(event) {
  console.log(event.target.value); // event 被隱含的標記為 any 型別
}
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

事件使用型別斷言

沒有標記型別時,這個 event 參數會隱含的標記為 any 型別。這也會在 tsconfig.json 中配置了 "strict": true 或 "noImplicitAny": true 時,導致 TypesScript 報錯。因此,建議明確為事件處理函式的參數標記型別。此外,我們需要強制轉換 event 上的屬性:

<script setup lang="ts">
function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value);
}
</script>

provide / inject 標記型別

InjectionKey 介面

provideinject 通常會在不同的元件之間使用。為了正確指定值的型別,Vue 提供了一個 InjectionKey 介面,它是一個泛型型別,繼承自 Symbol,可用於確保供應者和使用者之間注入值的型別一致:

import { provide, inject } from 'vue';
import type { InjectionKey } from 'vue';

const key = Symbol() as InjectionKey<string>;

provide(key, '肉鬆'); // 若提供的是非字串值會導致錯誤

const name = inject(key); // name 型別:string | undefined

建議將注入 key 的型別定義放在一個單獨的檔案中,這樣可以讓多個元件進行引入使用。

當使用字串注入 key 時,注入值的型別是 unknown,需要通過泛型參數明確宣告:

const name = inject<string>('肉鬆'); // 型別:string | undefined

需要注意的是,注入的值仍然可能是 undefined,因為無法保證供應者一定會在執行時提供 provide 的值。

當提供一個默認值後,undefined 型別就可以被移除:

const name  = inject<string>('肉鬆', '傑尼龜'); // 型別:string

強制轉換型別

如果我們確定該值將始終被提供,則可以強制轉換該值:

const name = inject('肉鬆') as string;

樣板引用標記型別

樣板引用需要透過明確指定的泛型參數以及一個初始值 null 來建立:

<script setup lang="ts">
import { ref, onMounted } from 'vue';

const el = ref<HTMLInputElement | null>(null);

onMounted(() => {
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>

需要注意的是,為了確保嚴格的型別安全,當訪問 el.value 時,建議使用可選串連型別保護。這是因為在元件被掛載之前,此 ref 的值將保持初始的 null,並且 v-if 行為而卸載參考的元素時,也可能導致其設定為 null

元件樣板引用標記型別

當我們需要為子元件新增一個樣板參考,以便呼叫它所公開的方法。例如,我們有一個子元件名為 MyModal,它提供了一個開啟視窗的方法:

// MyModal.vue
<script setup lang="ts">
import { ref } from 'vue';

const isOpenModal = ref(false);
const open = () => (isOpenModal.value = true);

defineExpose({
  open
})
</script>

為了取得 MyModal 的型別,我們首先需要透過 typeof 取得它的型別,然後使用 TypeScript 內建的 InstanceType 工具型別來取得它的實例型別:

// App.vue
<script setup lang="ts">
import MyModal from './MyModal.vue';

const modal = ref<InstanceType<typeof MyModal> | null>(null);

const openModal = () => {
  modal.value?.open();
}
</script>

本日重點

總結來說,Composition API 與 TypeScript 的結合為 Vue 元件開發帶來了更好的型別安全和程式碼。透過 setup 函式、型別定義、響應式資料、計算屬性等,我們能夠更清晰的定義元件邏輯,並確保程式碼品質。自訂 Hook、資料提供和注入、模組化開發以及錯誤處理進一步提高了開發效率和可維護性。

參考


上一篇
Day28 - 組織與管理程式碼的好夥伴 - Modules & Namespaces
下一篇
Day30 - 魔法結束囉!
系列文
TypeScript 魔法 - 喚醒你的程式碼靈感30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言