TS 在 Vue3 中的使用

TS配合Vue3的setup语法糖的类型定义,及一些第三方插件的TS类型定义,以下示例代码在unplugin-auto-import插件的帮助下,可以自动导入,无需手动导入。

一、setup 中的 TS 类型定义

1. props 类型标注

定义props时可以通过运行时声明或是类型声明进行,配合<script setup>语法糖,推荐使用基于类型的声明。

通过泛型参数来定义 props 的类型:

<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
  oth: YourType
}>()
</script>

也可以将 props 的类型移入一个单独的接口中:

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

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

语法限制 在 3.2 及以下版本中,defineProps()的泛型类型参数仅限于类型文字或对本地接口的引用。

当使用基于类型的声明时,失去了为 props 声明默认值的能力。这可以通过withDefaults编译器宏解决:

<script setup lang="ts">
interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})
</script>

2. emits 类型标注

<script setup>中,emit函数的类型标注也可以通过运行时声明或是类型声明进行:

运行时声明:

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

类型声明:

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

如果你使用的是vue3.3+,这里有更简洁的语法:

<script setup lang="ts">
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
</script>

3. ref() 类型标注

ref 会根据初始化时的值推导其类型:

// 推导出的类型:Ref<number>
const year = ref(2020)

// TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'

调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为:

// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // 成功!

有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref 这个类型:

import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // 成功!

个人觉得还是使用泛型参数更简洁一些。

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:

// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()

4. reactive() 类型标注

reactive()也会隐式地从它的参数中推导类型:

// 推导得到的类型:{ title: string }
const book = reactive({ title: 'TS在Vue3中的使用' })

要显式地标注一个 reactive 变量的类型,我们可以使用接口:

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'TS在Vue3中的使用' })

不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

5. computed() 类型标注

computed()会自动从其计算函数的返回值上推导出类型:

const count = ref(0)

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

// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')

由于 computed() 的是必须返回一个值,所以它的类型推导会比较准确,一般不需要显式地标注类型。

当然也可以通过泛型参数显式指定类型:

const double = computed<number>(() => {
  // 若返回值不是 number 类型则会报错
})

6. provide / inject 类型标注

provideinject通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个InjectionKey接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:

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

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

provide(key, 'foo') // 若提供的是非字符串值会导致错误

const foo = inject(key) // foo 的类型:string | undefined

建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。

当使用字符串注入key时,注入值的类型是unknown,需要通过泛型参数显式声明:

const foo = inject<string>('foo') // 类型:string | undefined

注意注入的值仍然可以是undefined,因为无法保证提供者一定会在运行时provide这个值。

当提供了一个默认值后,这个undefined类型就可以被移除:

const foo = inject<string>('foo', 'bar') // 类型:string

当然如果你确定该值将始终被提供,则还可以强制转换该值:

const foo = inject<string>('foo') as string // 类型:string

7. 模板引用 类型标注

模板引用需要通过一个显式指定的泛型参数和一个初始值null来创建:

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

<script setup lang="ts">
const div = ref<HTMLDivElement | null>(null)
const input = ref<HTMLInputElement | null>(null)
</script>

如果你的ref写在了子组件上,你想获取子组件的类型,以便调用它公开的方法或属性,我们首先需要通过typeof得到其类型,再使用 TypeScript 内置的InstanceType工具类型来获取其实例类型:

<!-- Parent.vue -->
<script setup lang="ts">
import Child from './Child.vue'

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

function openModal() {
  modal.value?.open()
}
</script>

8. 事件处理函数 类型标注

在处理原生DOM事件时,应该为我们传递给事件处理函数的参数正确地标注类型:

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

<script setup lang="ts">
function handleChange(event) {
  // `event` 隐式地标注为 `any` 类型
  console.log(event.target.value)
}
</script>

没有类型标注时,这个event参数会隐式地标注为any类型。这也会在tsconfig.json中配置了"strict": true"noImplicitAny": true时报出一个 TS 错误。因此,建议显式地为事件处理函数的参数标注类型。此外,你在访问event上的属性时可能需要使用类型断言

function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}

二、第三方插件的 TS 类型定义

1. ArcGIS API for JS

以创建Graphic为例:

import Graphic from '@arcgis/core/Graphic'

const graphic = new Graphic({
  geometry: {
    type: 'point', // => TS Error: 不能将类型“{ type: string; longitude: number; latitude: number; }”分配给类型“GeometryProperties”。对象字面量只能指定已知属性,并且“type”不在类型“GeometryProperties”中。
    longitude: 120.123,
    latitude: 36.456
  },
  symbol: {
    type: 'simple-marker', // 类似的错误
    color: 'red'
  }
})

以上添加到地图上是没有问题的,官方的文档也是这样写的,因为都是JS,但是在TS中,我们需要为geometrysymbol指定类型,我们只需要实例化对应的类即可:

import Graphic from '@arcgis/core/Graphic'
import Point from '@arcgis/core/geometry/Point'
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol'

const graphic = new Graphic({
  geometry: new Point({
    longitude: 120.123,
    latitude: 36.456
  }),
  symbol: new SimpleMarkerSymbol({
    color: 'red'
  })
})

适用于所有的ArcGIS API for JS中类似的情况。
ArcGIS API for JS文档:https://developers.arcgis.com/javascript/latest/api-reference/

2. ECharts + Vue-ECharts

以下面官方的Vue3示例为例:

<template>
  <VChart class="chart" :option="option" />
</template>

<script setup>
import { provide, ref } from 'vue'
import VChart, { THEME_KEY } from 'vue-echarts'

// 引入 echarts 核心模块中的注册方法use
import { use } from 'echarts/core'

// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers'

// 图表后缀都为 Chart
import { PieChart } from 'echarts/charts'

// 引入组件,组件后缀都为 Component
import {
  LegendComponent,
  TitleComponent,
  TooltipComponent
} from 'echarts/components'

use([
  CanvasRenderer,
  PieChart,
  TitleComponent,
  TooltipComponent,
  LegendComponent
])

provide(THEME_KEY, 'dark')

const option = ref({
  title: {
    text: 'Traffic Sources',
    left: 'center'
  },
  tooltip: {
    trigger: 'item',
    formatter: '{a} <br/>{b} : {c} ({d}%)'
  },
  legend: {
    orient: 'vertical',
    left: 'left',
    data: ['Direct', 'Email', 'Ad Networks', 'Video Ads', 'Search Engines']
  },
  series: [
    {
      name: 'Traffic Sources',
      type: 'pie',
      radius: '55%',
      center: ['50%', '60%'],
      data: [
        { value: 335, name: 'Direct' },
        { value: 310, name: 'Email' },
        { value: 234, name: 'Ad Networks' },
        { value: 135, name: 'Video Ads' },
        { value: 1548, name: 'Search Engines' }
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ]
})
</script>

<style scoped>
.chart {
  height: 400px;
}
</style>

在写option时会没有任何提示,option的类型定义我们需要自己从echarts中导入:

// 用来组合Option 类型的 ComposeOption
import type { ComposeOption } from 'echarts/core'

// 图表系列类型的定义后缀都为 SeriesOption
import type { PieSeriesOption } from 'echarts/charts'

// 组件类型的定义后缀都为 ComponentOption
import type {
  LegendComponentOption,
  TitleComponentOption,
  TooltipComponentOption
} from 'echarts/components'

// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type EChartsOption = ComposeOption<
  | TitleComponentOption
  | TooltipComponentOption
  | LegendComponentOption
  | PieSeriesOption
>

// 使用
const options = ref<EChartsOption>({
  // ...现在就有了提示
})

ECharts文档:https://echarts.apache.org/zh/index.html
Vue-ECharts文档:https://github.com/ecomfe/vue-echarts#readme

3. Axios

import axios from 'axios'
import type { AxiosResponse } from 'axios'

// 定义返回数据的类型,一般是后端返回的数据结构,比较通用,可以放在一个单独的文件中
interface Res<T> {
  code: number
  data: T
  // ...其他字段
}

// 根据具体接口定义返回数据的类型
interface ReturnData {
  id: number
  age: string
  parent: string
  // ...其他字段
}

// 接口参数类型
interface YourData {
  id: number
  name: string
  // ...其他字段
}

// 请求函数通常放在单独文件中,使用时导入,以 post 请求为例
async function fetchData(data: YourData) {
  const res = await axios<Res<ReturnData>>({
    method: 'post',
    url: 'your url',
    data,
  })

  // 如果只需要后端返回的数据,可以直接 return res.data
  // 这样下面 then 中的res的类型就是 Res<ReturnData>
  return res
}

// ts会自动推导出返回数据的类型
const data: YourData = { id: 1, name: 'Ju Peng' }
fetchData(data).then((res) => {
  console.log(res.data) // res.data 的类型为 ResData
  console.log(res.data.data) // data.data 的类型为 ReturnData
})

Axios文档:https://www.axios-http.cn

4. VueRequest

VueRequest一般与axios配合使用,你只需要定义好axios请求函数,然后使用useRequest即可,ts会自动推导出返回数据的类型:

import { useRequest } from 'vue-request'

import { fetchData } from './api' // 使用上面 axios 定义的请求函数

const { data, run } = useRequest(fetchData, {
  manual: true,
  defaultParams: { id: 1, name: 'Ju Peng' }, // 默认参数,必须是 YourData 类型,否则会报错
  onSuccess: (res) => {
    console.log(res.data) // res.data 的类型为 ResData
    console.log(res.data.data) // data.data 的类型为 ReturnData
  }
})

run({ id: 1, name: 'Ju Peng' }) // 调用传参数,必须是 YourData 类型,否则会报错

// data 的类型为 Ref<AxiosResponse<Res<ReturnData>>>
console.log(data.value.data) // data.value.data 的类型为 ResData
console.log(data.value.data.data) // data.value.data.data 的类型为 ReturnData

VueRequest文档:https://www.attojs.com/