Skip to content

<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。