主题
<script setup> 与 TypeScript 使用笔记
汇总
<script setup>配合 TypeScript 时的常用写法:响应式 API 的类型推导、编译宏的类型声明、provide / inject、事件类型、自定义指令与顶层 await。 官方文档:<script setup>、TypeScript 支持。
一、基本特性
1. 执行时机
在组件创建之前执行,不能通过 this 获取组件实例,需要实例时用 getCurrentInstance()。
2. 顶层绑定直接暴露给模板
顶层的变量、函数以及 import 导入的内容,都能在模板中直接使用。
二、ref
ref 支持传入一个泛型参数。
1. 值 ref
ts
// 默认推导出 string 类型
const initCode = ref("200");
// 或者手动定义更复杂的类型
const initCode = ref<string | number>("200");2. 模板 ref
vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
const el = ref<HTMLDivElement | null>(null);
onMounted(() => {
console.log(el.value);
});
</script>
<template>
<div ref="el">div 区域</div>
</template>3. 组件 ref
vue
<template>
<HelloWorld ref="helloworld" />
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
const helloworld = ref<InstanceType<typeof HelloWorld> | null>(null);
onMounted(() => {
// 调用子组件的 handleClick 方法
helloworld.value?.handleClick();
});
</script>提示:如果子组件使用
<script setup lang="ts">,默认是「全封闭」的,父组件拿不到任何东西;子组件需要用defineExpose声明父组件可以访问的属性。
三、reactive
1. 定义接口
新建 src/types/user.ts(在 types 文件夹下新建 user.ts):
ts
export interface User {
name?: string;
age: number;
}2. 使用
vue
<script setup lang="ts">
import { reactive } from "vue";
import type { User } from "@/types/user";
// reactive 会隐式地从它的参数中推导类型
// 也可以使用接口直接给变量定义类型
const user: User = reactive({
name: "zhangsan",
age: 20,
});
</script>提示:不推荐使用
reactive()的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
四、computed
computed() 会自动从其计算函数的返回值上推导出类型,也可以通过泛型参数显式指定:
ts
const age = computed<number>(() => {
return 2;
});五、defineProps
用于定义 props 类型,直接使用,不需要 import 引入。
1. 基本使用
vue
<script setup lang="ts">
// 3.5 之前
const props = defineProps<{ name: string }>();
// 3.5 之后
const { name } = defineProps<{ name: string }>();
</script>提示:在 3.5 版本中,props 已支持解构,但不支持深层解构。
ts
// 不支持深层解构
const {
obj: { a: b },
} = defineProps<{ obj: { a: { b: number } } }>();2. 类型由外部传入
在 3.3 版本中支持 TypeScript 类型由外部传入:
vue
<script setup lang="ts">
// 使用外部导入的类型
import type { User } from "@/types/user";
const { name } = defineProps<User>();
</script>3. 泛型参数
在 3.3 版本中支持,使用 generic 声明泛型:
vue
<script setup lang="ts" generic="T">
defineProps<{
items: T[];
selected: T;
}>();
</script>泛型约束:
vue
<script setup lang="ts" generic="T extends string | number, U extends Item">
import type { Item } from "./types";
defineProps<{
id: T;
list: U[];
}>();
</script>4. 定义默认值
3.5 之前,使用 withDefaults 定义默认值:
vue
<script setup lang="ts">
// 使用外部导入的类型
import type { User } from "@/types/user";
const { name, age } = withDefaults(defineProps<User>(), {
name: "hello",
age: 10,
});
</script>3.5 之后,在解构的时候直接设置默认值:
vue
<script setup lang="ts">
const { name = "zhangsan" } = defineProps<{ name: string }>();
</script>六、defineEmits
用于定义 emits 类型,直接使用,不需要 import 引入。
父组件:
vue
<template>
<div>
<HelloWorld @change="handleChange" />
</div>
</template>
<script setup lang="ts">
import HelloWorld from "@/components/HelloWorld.vue";
import type { User } from "@/types/user";
const handleChange = (value: User) => {
console.log(value);
};
</script>子组件:
vue
<template>
<div>
<button @click="handleClick">按钮</button>
</div>
</template>
<script setup lang="ts">
import type { User } from "@/types/user";
const emit = defineEmits<{ change: [value: User] }>();
// 目前 eslint 存在问题,可直接使用 defineEmits(['change'])
const handleClick = () => {
emit("change", { name: "2", age: 21 });
};
</script>七、defineExpose
用于定义父组件通过模板 ref 能获取到的属性。接着上面「组件 ref」的案例修改子组件 HelloWorld:
vue
<template>
<div></div>
</template>
<script setup lang="ts">
const handleClick = () => {
console.log("子组件方法");
};
defineExpose({ handleClick });
</script>八、defineOptions
在 <script setup> 中声明组件选项,而不必使用单独的 <script> 块:
vue
<script setup lang="ts">
defineOptions({
name: "MyComponent",
});
</script>九、defineSlots
1. 默认作用域插槽
父组件:
vue
<template>
<div>
<HelloWorld>
<template #="{ msg }"> {{ msg }} </template>
</HelloWorld>
</div>
</template>
<script lang="ts" setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>子组件:
vue
<template>
<h1>
<slot msg="12"></slot>
</h1>
</template>
<script setup lang="ts">
defineSlots<{
default(props: { msg: string }): any;
}>();
</script>2. 具名作用域插槽
父组件:
vue
<template>
<div>
<HelloWorld>
<template #todo="{ msg }"> {{ msg }} </template>
</HelloWorld>
</div>
</template>
<script lang="ts" setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>子组件:
vue
<template>
<h1>
<slot msg="12" name="todo"></slot>
</h1>
</template>
<script setup lang="ts">
defineSlots<{
todo(props: { msg: string }): any;
}>();
</script>十、provide / inject
1. key 是 Symbol
新建 src/constant/index.ts:
ts
import type { InjectionKey } from "vue";
// key 对应的值只能为 string 类型
export const key = Symbol() as InjectionKey<string>;父组件使用 provide 提供值:
vue
<script setup lang="ts">
import { provide } from "vue";
import { key } from "@/constant/index";
provide(key, "123");
</script>子组件使用 inject 取值:
vue
<script setup lang="ts">
import { inject } from "vue";
import { key } from "@/constant/index";
const string = inject(key);
</script>2. key 是字符串
inject 返回的类型是 unknown,需要通过泛型参数显式声明。
父组件提供值:
vue
<script setup lang="ts">
import { ref, provide } from "vue";
const state = ref(0);
const handlerState = () => {
state.value = 1;
};
provide("info", state); // 提供响应式对象
provide("func", handlerState); // 提供改变响应式对象的方法
</script>子组件取值:
vue
<script setup lang="ts">
import { inject } from "vue";
// 通过泛型参数显式声明
const state = inject<number>("info");
const func = inject<() => void>("func");
</script>3. undefined 问题
由于无法保证 provide 一定会提供这个值,因此即使通过泛型参数显式声明了类型,inject 的返回值仍会多一个 undefined 类型。两种处理方式:
ts
// 1. 提供默认值,可消除 undefined
const state = inject<number>("info", 20);
// 2. 使用类型断言,告诉编辑器这个值一定会提供
const state = inject("info") as number;十一、事件类型
1. input change 事件
vue
<template>
<input type="text" @change="handleChange" />
</template>
<script setup lang="ts">
const handleChange = (evt: Event) => {
console.log((evt.target as HTMLInputElement).value);
};
</script>2. button click 事件
vue
<template>
<button @click="handleClick">按钮</button>
</template>
<script setup lang="ts">
const handleClick = (evt: Event) => {
// 获取按钮的样式信息
console.log((evt.target as HTMLButtonElement).style);
};
</script>3. HTML 标签与元素类型映射
HTMLElementTagNameMap 维护了 HTML 标签名到具体 DOM 元素类型的映射,querySelector 等 API 依靠它把标签名推导成对应的元素类型:
ts
interface HTMLElementTagNameMap {
a: HTMLAnchorElement;
abbr: HTMLElement;
address: HTMLElement;
area: HTMLAreaElement;
article: HTMLElement;
aside: HTMLElement;
audio: HTMLAudioElement;
b: HTMLElement;
base: HTMLBaseElement;
bdi: HTMLElement;
bdo: HTMLElement;
blockquote: HTMLQuoteElement;
body: HTMLBodyElement;
br: HTMLBRElement;
button: HTMLButtonElement;
canvas: HTMLCanvasElement;
caption: HTMLTableCaptionElement;
cite: HTMLElement;
code: HTMLElement;
col: HTMLTableColElement;
colgroup: HTMLTableColElement;
data: HTMLDataElement;
datalist: HTMLDataListElement;
dd: HTMLElement;
del: HTMLModElement;
details: HTMLDetailsElement;
dfn: HTMLElement;
dialog: HTMLDialogElement;
div: HTMLDivElement;
dl: HTMLDListElement;
dt: HTMLElement;
em: HTMLElement;
embed: HTMLEmbedElement;
fieldset: HTMLFieldSetElement;
figcaption: HTMLElement;
figure: HTMLElement;
footer: HTMLElement;
form: HTMLFormElement;
h1: HTMLHeadingElement;
h2: HTMLHeadingElement;
h3: HTMLHeadingElement;
h4: HTMLHeadingElement;
h5: HTMLHeadingElement;
h6: HTMLHeadingElement;
head: HTMLHeadElement;
header: HTMLElement;
hgroup: HTMLElement;
hr: HTMLHRElement;
html: HTMLHtmlElement;
i: HTMLElement;
iframe: HTMLIFrameElement;
img: HTMLImageElement;
input: HTMLInputElement;
ins: HTMLModElement;
kbd: HTMLElement;
label: HTMLLabelElement;
legend: HTMLLegendElement;
li: HTMLLIElement;
link: HTMLLinkElement;
main: HTMLElement;
map: HTMLMapElement;
mark: HTMLElement;
menu: HTMLMenuElement;
meta: HTMLMetaElement;
meter: HTMLMeterElement;
nav: HTMLElement;
noscript: HTMLElement;
object: HTMLObjectElement;
ol: HTMLOListElement;
optgroup: HTMLOptGroupElement;
option: HTMLOptionElement;
output: HTMLOutputElement;
p: HTMLParagraphElement;
picture: HTMLPictureElement;
pre: HTMLPreElement;
progress: HTMLProgressElement;
q: HTMLQuoteElement;
rp: HTMLElement;
rt: HTMLElement;
ruby: HTMLElement;
s: HTMLElement;
samp: HTMLElement;
script: HTMLScriptElement;
search: HTMLElement;
section: HTMLElement;
select: HTMLSelectElement;
slot: HTMLSlotElement;
small: HTMLElement;
source: HTMLSourceElement;
span: HTMLSpanElement;
strong: HTMLElement;
style: HTMLStyleElement;
sub: HTMLElement;
summary: HTMLElement;
sup: HTMLElement;
table: HTMLTableElement;
tbody: HTMLTableSectionElement;
td: HTMLTableCellElement;
template: HTMLTemplateElement;
textarea: HTMLTextAreaElement;
tfoot: HTMLTableSectionElement;
th: HTMLTableCellElement;
thead: HTMLTableSectionElement;
time: HTMLTimeElement;
title: HTMLTitleElement;
tr: HTMLTableRowElement;
track: HTMLTrackElement;
u: HTMLElement;
ul: HTMLUListElement;
var: HTMLElement;
video: HTMLVideoElement;
wbr: HTMLElement;
}十二、自定义指令
以 vName 的形式来命名本地自定义指令:
vue
<template>
<h1 v-title>This is a Heading</h1>
</template>
<script setup lang="ts">
const vTitle = {
beforeMount: (el: HTMLHeadingElement) => {
// 在元素上做些操作
console.log(el);
},
};
</script>十三、顶层 await
<script setup> 中可以使用顶层 await,代码会被编译成 async setup():
vue
<script setup>
const post = await fetch("/api/post/1").then((r) => r.json());
</script>注意:
async setup()必须与Suspense组合使用。- Vue Router 的懒加载和 Vue 3 所支持的异步组件是不同的。用 Vue Router 配置路由时,不应该使用
defineAsyncComponent或async setup。