Skip to content

Commit 533411a

Browse files
committed
docs(vue): 补充 vue typescript 内容
1 parent 4ebb6c2 commit 533411a

File tree

2 files changed

+328
-0
lines changed

2 files changed

+328
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
---
2+
order: 20
3+
category:
4+
- 笔记
5+
- frontend
6+
- vue
7+
tag:
8+
- vue3
9+
---
10+
11+
# TypeScript 与组合式 API
12+
13+
## 为组件的 props 标注类型
14+
15+
## 使用 `<script setup>`
16+
17+
当使用 `<script setup>` 时,`defineProps()` 宏函数支持从它的参数中推导类型:
18+
19+
```vue
20+
<script setup lang="ts">
21+
const props = defineProps({
22+
foo: {
23+
type: String,
24+
required: true
25+
},
26+
bar: Number
27+
})
28+
29+
props.foo // string
30+
props.bar // number | undefined
31+
</script>
32+
```
33+
34+
这被称之为“运行时声明”,因为传递给 `defineProps()` 的参数会作为运行时的 `props` 选项使用。
35+
36+
然而,通过泛型参数来定义 `props` 的类型通常更直接:
37+
38+
```vue
39+
<script setup lang="ts">
40+
const props = defineProps<{
41+
foo: string
42+
bar?: number
43+
}>()
44+
</script>
45+
```
46+
47+
这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。
48+
49+
基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。
50+
51+
我们也可以将 props 的类型移入一个单独的接口中:
52+
53+
```vue
54+
<script setup lang="ts">
55+
interface Props {
56+
foo: string
57+
bar?: number
58+
}
59+
60+
const props = defineProps<Props>()
61+
</script>
62+
```
63+
64+
这同样适用于 Props 从另一个源文件中导入的情况。该功能要求 TypeScript 作为 Vue 的一个 peer dependency。
65+
66+
```vue
67+
<script setup lang="ts">
68+
import type { Props } from './Props'
69+
70+
const props = defineProps<Props>()
71+
</script>
72+
```
73+
74+
## Props 解构默认值
75+
76+
当使用基于类型的声明时,我们失去了为 `props` 声明默认值的能力。这可以通过 `withDefaults` 编译器宏解决:
77+
78+
```ts
79+
export interface Props {
80+
msg?: string
81+
labels?: string[]
82+
}
83+
84+
const props = withDefaults(defineProps<Props>(), {
85+
msg: 'hello',
86+
labels: () => ['one', 'two']
87+
})
88+
```
89+
90+
这将被编译为等效的运行时 props default 选项。此外,`withDefaults` 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。
91+
92+
::: tip
93+
请注意,可变引用类型 (如数组或对象) 的默认值应封装在函数中,以避免被意外修改或产生外部副作用。这样可以确保每个组件实例都能获得属于自己的默认值副本。
94+
:::
95+
96+
### `<script setup>` 场景下
97+
98+
如果没有使用 `<script setup>`,那么为了开启 props 的类型推导,必须使用 `defineComponent()`。传入 `setup()` 的 props 对象类型是从 props 选项中推导而来。
99+
100+
```ts
101+
import { defineComponent } from 'vue'
102+
103+
export default defineComponent({
104+
props: {
105+
message: String
106+
},
107+
setup(props) {
108+
props.message // <-- 类型:string
109+
}
110+
})
111+
```
112+
113+
### 复杂的 prop 类型
114+
115+
通过基于类型的声明,一个 prop 可以像使用其他任何类型一样使用一个复杂类型:
116+
117+
```vue
118+
<script setup lang="ts">
119+
interface Book {
120+
title: string
121+
author: string
122+
year: number
123+
}
124+
125+
const props = defineProps<{
126+
book: Book
127+
}>()
128+
</script>
129+
```
130+
131+
对于运行时声明,我们可以使用 `PropType` 工具类型:
132+
133+
```ts
134+
import type { PropType } from 'vue'
135+
136+
const props = defineProps({
137+
book: Object as PropType<Book>
138+
})
139+
```
140+
141+
其工作方式与直接指定 props 选项基本相同:
142+
143+
```ts
144+
import { defineComponent } from 'vue'
145+
import type { PropType } from 'vue'
146+
147+
export default defineComponent({
148+
props: {
149+
book: Object as PropType<Book>
150+
}
151+
})
152+
```
153+
154+
## 为组件的 emits 标注类型
155+
156+
`<script setup>` 中,`emit` 函数的类型标注也可以通过运行时声明或是类型声明进行:
157+
158+
```vue
159+
<script setup lang="ts">
160+
// 运行时
161+
const emit = defineEmits(['change', 'update'])
162+
</script>
163+
```
164+
165+
```vue
166+
<script setup lang="ts">
167+
// 基于选项
168+
const emit = defineEmits({
169+
change: (id: number) => {
170+
// 返回 `true` 或 `false`
171+
// 表明验证通过或失败
172+
return true
173+
},
174+
update: (value: string) => {
175+
// 返回 `true` 或 `false`
176+
// 表明验证通过或失败
177+
return true
178+
}
179+
})
180+
</script>
181+
```
182+
183+
````vue
184+
<script setup lang="ts">
185+
// 基于类型
186+
const emit = defineEmits<{
187+
(e: 'change', id: number): void
188+
(e: 'update', value: string): void
189+
}>()
190+
</script>
191+
192+
```vue
193+
<script setup lang="ts">
194+
// 3.3+: 可选的、更简洁的语法
195+
const emit = defineEmits<{
196+
change: [id: number]
197+
update: [value: string]
198+
}>()
199+
</script>
200+
````
201+
202+
类型参数可以是以下的一种:
203+
204+
1. 一个可调用的函数类型,但是写作一个包含调用签名的类型字面量。它将被用作返回的 emit 函数的类型。
205+
2. 一个类型字面量,其中键是事件名称,值是数组或元组类型,表示事件的附加接受参数。上面的示例使用了具名元组,因此每个参数都可以有一个显式的名称。
206+
207+
若没有使用 `<script setup>``defineComponent()` 也可以根据 `emits` 选项推导暴露在 setup 上下文中的 emit 函数的类型:
208+
209+
```ts
210+
import { defineComponent } from 'vue'
211+
212+
export default defineComponent({
213+
emits: ['change'],
214+
setup(props, { emit }) {
215+
emit('change') // <-- 类型检查 / 自动补全
216+
}
217+
})
218+
```
219+
220+
## 为 provide / inject 标注类型
221+
222+
`provide``inject` 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 `InjectionKey` 接口,它是一个继承自 `Symbol` 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:
223+
224+
```ts
225+
import { inject, provide } from 'vue'
226+
import type { InjectionKey } from 'vue'
227+
228+
const key = Symbol() as InjectionKey<string>
229+
230+
provide(key, 'foo') // 若提供的是非字符串值会导致错误
231+
232+
const foo = inject(key) // foo 的类型:string | undefined
233+
```
234+
235+
::: tip
236+
建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。
237+
:::
238+
239+
当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:
240+
241+
```ts
242+
const foo = inject<string>('foo') // 类型:string | undefined
243+
```
244+
245+
注意注入的值仍然可以是 `undefined`,因为无法保证提供者一定会在运行时 `provide` 这个值。
246+
247+
当提供了一个默认值后,这个 `undefined` 类型就可以被移除:
248+
249+
```ts
250+
const foo = inject<string>('foo', 'bar') // 类型:string
251+
```
252+
253+
如果你确定该值将始终被提供,则还可以强制转换该值:
254+
255+
```ts
256+
const foo = inject('foo') as string // 类型:string
257+
```
258+
259+
## 为模板引用标注类型
260+
261+
模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建:
262+
263+
```vue {4}
264+
<script setup lang="ts">
265+
import { onMounted, ref } from 'vue'
266+
267+
const el = ref<HTMLInputElement | null>(null)
268+
269+
onMounted(() => {
270+
el.value?.focus()
271+
})
272+
</script>
273+
274+
<template>
275+
<input ref="el">
276+
</template>
277+
```
278+
279+
::: warning
280+
为了严格的类型安全,有必要在访问 `el.value` 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 `ref` 的值都是初始的 `null`,并且在由于 `v-if` 的行为将引用的元素卸载时也可以被设置为 `null`
281+
:::
282+
283+
## 为组件模板引用标注类型
284+
285+
有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 `MyModal` 子组件,它有一个打开模态框的方法:
286+
287+
```vue
288+
<!-- MyModal.vue -->
289+
<script setup lang="ts">
290+
import { ref } from 'vue'
291+
292+
const isContentShown = ref(false)
293+
const open = () => isContentShown.value = true
294+
295+
defineExpose({
296+
open
297+
})
298+
</script>
299+
```
300+
301+
为了获取 `MyModal` 的类型,我们首先需要通过 `typeof` 得到其类型,再使用 TypeScript 内置的 `InstanceType` 工具类型来获取其实例类型:
302+
303+
```vue
304+
<!-- App.vue -->
305+
<script setup lang="ts">
306+
import { ref } from 'vue'
307+
import MyModal from './MyModal.vue'
308+
309+
const modal = ref<InstanceType<typeof MyModal> | null>(null)
310+
311+
function openModal() {
312+
modal.value.open()
313+
}
314+
</script>
315+
```
316+
317+
如果组件的具体类型无法获得,或者你并不关心组件的具体类型,那么可以使用 `ComponentPublicInstance`。这只会包含所有组件都共享的属性,比如 `$el`
318+
319+
```vue
320+
<!-- App.vue -->
321+
<script setup lang="ts">
322+
import { ref } from 'vue'
323+
import type { ComponentPublicInstance } from 'vue'
324+
325+
const child = ref<ComponentPublicInstance | null>(null)
326+
</script>
327+
```

docs/notes/frontend/vue/vue3/typescript/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
order: 10
23
category:
34
- 笔记
45
- frontend

0 commit comments

Comments
 (0)