简言
现在vue3和typescript搭配使用是一个较常见的方案,下面参考vue3官网总结下在vue项目中使用ts(TypeScript)的方法。
typescript配置
新建项目
如果你准备新建vue3项目,那么使用create-vue官方脚手架,它提供了搭建基于 Vite 且 TypeScript 就绪的 Vue 项目的选项。
已有项目
如果是已有项目,则需要手动添加ts相关的依赖:
npm install typescript vue-tsc --save-dev 或 npm install typescript vue-tsc -D
复制
typescript是ts的依赖,vue-tsc是对 TypeScript 自身命令行界面 tsc 的一个封装。ts是开发采用的所以添加到开发环境(devDependencies)中。
新建ts的配置文件tsconfig.json。
下面是我的,以供参考:
{ "compilerOptions": { "target": "es6", "useDefineForClassFields": true, "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "declaration":true, "lib": ["es6", "dom"], "skipLibCheck": true, "baseUrl": "./", "paths": { "@/*":["src/*"], "#/*":["types/*"], } }, "include": ["src/env.d.ts","src/**/*.js","src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], }
复制
compilerOptions中有几个属性是必需的:
- isolatedModules为true。(可用verbatimModuleSyntax为true代替)
- trict 设置为 true
- 如果你在构建工具中配置了路径解析别名,例如 @/* 这个别名被默认配置在了 create-vue 项目中,你需要通过 compilerOptions.paths 选项为 TypeScript 再配置一遍。
- 如果你打算在 Vue 中使用 TSX,请将 compilerOptions.jsx 设置为 “preserve”,并将 compilerOptions.jsxImportSource 设置为 “vue”。
vue页面中使用ts
选项式
为了让 TypeScript 正确地推导出组件选项内的类型,我们需要通过 defineComponent() 这个全局 API 来定义组件:
<script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ // 启用了类型推导 props: { name: String, msg: { type: String, required: true } }, data() { return { count: 1 } }, mounted() { this.name // 类型:string | undefined this.msg // 类型:string this.count // 类型:number } }) </script>
复制
要在单文件组件中使用 TypeScript,需要在 <script> 标签上加上 lang=“ts” 的 attribute。当 lang=“ts” 存在时,所有的模板内表达式都将享受到更严格的类型检查。
props标注类型
选项式 API 中对 props 的类型推导需要用 defineComponent() 来包装组件。有了它,Vue 才可以通过 props 以及一些额外的选项,比如 required: true 和 default 来推导出 props 的类型:
import { defineComponent } from 'vue' export default defineComponent({ // 启用了类型推导 props: { name: String, id: [Number, String], msg: { type: String, required: true }, metadata: null }, mounted() { this.name // 类型:string | undefined this.id // 类型:number | string | undefined this.msg // 类型:string this.metadata // 类型:any } })
复制
当属性是一个对象或函数时,使用PropType 这个工具类型来标记类型:
import { defineComponent } from 'vue' import type { PropType } from 'vue' interface Book { title: string author: string year: number } export default defineComponent({ props: { book: { // 提供相对 `Object` 更确定的类型 type: Object as PropType<Book>, required: true }, // 也可以标记函数 callback: Function as PropType<(id: number) => void> }, mounted() { this.book.title // string this.book.year // number // TS Error: argument of type 'string' is not // assignable to parameter of type 'number' this.callback?.('123') } })
复制
选择使用函数指定默认值时,尽量使用箭头函数作为 prop 的 validator 和 default 选项值
emits 标注类型
emits需要提供一个对象来定义要触发的事件,触发使用未定义的事件将会抛出类型错误:
import { defineComponent } from 'vue' export default defineComponent({ emits: { addBook(payload: { bookName: string }) { // 执行运行时校验 return payload.bookName.length > 0 } } , // emits对象 methods: { onSubmit() { this.$emit('addBook', { bookName: 123 // 类型错误 }) this.$emit('non-declared-event') // 类型错误 } } })
复制
计算属性标记类型
计算属性会自动根据其返回值来推导其类型,也可以显式标注返回类型:
import { defineComponent } from 'vue' export default defineComponent({ data() { return { message: 'Hello!' } }, computed: { greeting() { return this.message + '!' // string }, // 显式标注返回类型 greeting2(): string { return this.message + '2!' }, // 标注一个可写的计算属性 greetingUppercased: { get(): string { return this.greeting.toUpperCase() }, set(newValue: string) { this.message = newValue.toUpperCase() } } }, mounted() { this.greeting // 类型:string } })
复制
事件函数标注类型
在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型。
import { defineComponent } from 'vue' export default defineComponent({ methods: { handleChange(event: Event) { console.log((event.target as HTMLInputElement).value) }, click(type:number){ console.log(type); // type类型:number } } })
复制
组合式
lang=“ts” 也可以用于
<script setup lang="ts"> let x: string | number = 1 </script> <template> {{ (x as number).toFixed(2) }} </template>
复制
<template> 在绑定表达式中也支持 TypeScript。这对需要在模板表达式中执行类型转换(as)的情况下非常有用。
props 标注类型
当使用 <script setup> 时,defineProps() 宏函数支持从它的参数中推导类型:
<script setup lang="ts"> const props = defineProps({ foo: { type: String, required: true }, bar: Number }) props.foo // string props.bar // number | undefined </script>
复制
这被称之为“运行时声明”,因为传递给 defineProps() 的参数会作为运行时的 props 选项使用。复杂的类型(函数或对象)需要借助 PropType 工具类型。
import type { PropType } from 'vue' interface Book { title: string author: string year: number } const props = defineProps({ book: Object as PropType<Book> })
复制
也可以通过泛型参数来定义 props 的类型,这被称之为“基于类型的声明”。
<script setup lang="ts"> const props = defineProps<{ foo: string bar?: number }>() </script>
复制
编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。
基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。
我们也可以将 props 的类型移入一个单独的接口中(个人推荐):
<script setup lang="ts"> interface Book { title: string author: string year: number } interface Props { foo: string bar?: number book:Book // 对象类型 } const props = defineProps<Props>() </script>
复制
emits 标注类型
在 <script setup> 中,emit 函数的类型标注也可以通过运行时声明或是类型声明进行:
<script setup lang="ts"> // 运行时 const emit = defineEmits(['change', 'update']) // 基于选项 const emit = defineEmits({ change: (id: number) => { // 返回 `true` 或 `false` // 表明验证通过或失败 }, update: (value: string) => { // 返回 `true` 或 `false` // 表明验证通过或失败 } }) // 基于类型 const emit = defineEmits<{ (e: 'change', id: number): void (e: 'update', value: string): void }>() // 3.3+: 可选的、更简洁的语法 const emit = defineEmits<{ change: [id: number] update: [value: string] }>() </script>
复制
类型参数可以是以下的一种:
- 一个可调用的函数类型,但是写作一个包含调用签名的类型字面量。它将被用作返回的 emit 函数的类型。
- 一个类型字面量,其中键是事件名称,值是数组或元组类型,表示事件的附加接受参数。上面的示例使用了具名元组,因此每个参数都可以有一个显式的名称。
我们可以看到,基于类型的声明使我们可以对所触发事件的类型进行更细粒度的控制。
个人推荐基于类型声明方式,简单明了
ref 标注类型
ref 默认会根据初始化时的值推导其类型,不赋值则推到为undefined。
可以通过使用 Ref 这个类型指定一个更复杂的类型:
import { ref } from 'vue' import type { Ref } from 'vue' const year: Ref<string | number> = ref('2020') year.value = 2020 // 成功!
复制
或者,在调用 ref() 时传入一个泛型参数(个人推荐),来覆盖默认的推导行为:
// 得到的类型:Ref<string | number> const year = ref<string | number>('2020') year.value = 2020 // 成功! // 推导得到的类型:Ref<number | undefined> const n = ref<number>()
复制
reactive() 标注类型
reactive() 也会隐式地从它的参数中推导类型。
要显式地标注一个 reactive 变量的类型,我们可以使用接口:
import { reactive } from 'vue' interface Book { title: string year?: number } const book: Book = reactive({ title: 'Vue 3 指引' })
复制
不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
computed() 标注类型
computed() 会自动从其计算函数的返回值上推导出类型。
你还可以通过泛型参数显式指定类型:
import { ref, computed } from 'vue' 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('') // 泛型参数 const double2 = computed<number>(() => { // 若返回值不是 number 类型则会报错 })
复制
事件处理函数标注类型
就是ts中的的事件类型声明。
function handleChange(event: Event) { console.log((event.target as HTMLInputElement).value) }
复制
provide / inject 标注类型
provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:
import { provide, inject } from 'vue' import type { InjectionKey } from 'vue' const key = Symbol() as InjectionKey<string> provide(key, 'foo') // 若提供的是非字符串值会导致错误 const foo = inject(key) // foo 的类型:string | undefined
复制
建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。
key是Symbol,所以用同一个文件管理。
当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:
const foo = inject<string>('foo') // 类型:string | undefined // 当提供了一个默认值后,这个 undefined 类型就可以被移除: const foo2 = inject<string>('foo', 'bar') // 类型:string // 强制转换 const foo3 = inject('foo') as string
复制
注意注入的值仍然可以是 undefined,因为无法保证提供者一定会在运行时 provide 这个值。
DOM模板引用标注类型
模板引用需要通过一个显式指定的泛型参数和一个初始值 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 时使用可选链或类型守卫。
组件模板引用标注类型
有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。
举例来说,我们有一个 MyModal 子组件,它有一个打开模态框的方法:
<!-- MyModal.vue --> <script setup lang="ts"> import { ref } from 'vue' const isContentShown = ref(false) const open = () => (isContentShown.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>
复制
如果组件的具体类型无法获得,或者你并不关心组件的具体类型,那么可以使用 ComponentPublicInstance。这只会包含所有组件都共享的属性,比如 $el。
import { ref } from 'vue' import type { ComponentPublicInstance } from 'vue' const child = ref<ComponentPublicInstance | null>(null)
复制
泛型
可以使用
<script setup lang="ts" generic="T extends string | number, U extends Item" > import type { Item } from './types' defineProps<{ id: T list: U[] }>() </script>
复制
扩展vue类型
可以新建一个vue.d.ts声明文件专门管理vue的类型扩展,记得在ts配置文件引入。
扩展全局属性
某些插件会通过 app.config.globalProperties 为所有组件都安装全局可用的属性。这些属性omponentCustomProperties 接口来在vue声明空间中扩展定义。
例如:
import axios from 'axios' declare module 'vue' { interface ComponentCustomProperties { $http: typeof axios $translate: (key: string) => string } }
复制
这样 全局属性 $http和$trabslate就有了类型。
我们可以将这些类型扩展放在一个 .ts 文件,或是一个影响整个项目的 *.d.ts 文件中。无论哪一种,都应确保在 tsconfig.json 中包括了此文件。
扩展自定义选项
某些插件,比如 vue-router,提供了一些自定义的组件选项,比如 beforeRouteEnter:
import { defineComponent } from 'vue' export default defineComponent({ beforeRouteEnter(to, from, next) { // ... } })
复制
如果没有确切的类型标注,这个钩子函数的参数会隐式地标注为 any 类型。我们可以为 ComponentCustomOptions 接口扩展自定义的选项来支持。同样的也是在vue类型声明空间来扩展:
import { Route } from 'vue-router' declare module 'vue' { interface ComponentCustomOptions { beforeRouteEnter?(to: Route, from: Route, next: () => void): void } }
复制
结语
结束了。