这被称之为“运行时声明”,因为传递给 defineProps()
的参数会作为运行时的 props
选项使用。
然而,通过泛型参数来定义 props 的类型通常更直接:
这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。
基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。
我们也可以将 props
的类型移入一个单独的接口中:
为了生成正确的运行时代码,传给 defineProps() 的泛型参数必须是以下之一:
defineProps<{ /*... */ }>()
interface Props {/* ... */}defineProps()
接口或对象字面类型可以包含从其他文件导入的类型引用,但是,传递给 defineProps 的泛型参数本身不能是一个导入的类型:
import { Props } from './other-file'// 不支持!
defineProps()
这是因为 Vue 组件是单独编译的,编译器目前不会抓取导入的文件以分析源类型。我们计划在未来的版本中解决这个限制。
当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:
export interface Props {msg?: stringlabels?: string[]
}const props = withDefaults(defineProps(), {msg: 'hello',labels: () => ['one', 'two']
})
这将被编译为等效的运行时 props default 选项。此外,withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。
或者,你可以使用目前为实验性的响应性语法糖:
这个行为目前需要显式地选择开启。
场景下如果没有使用 ,那么为了开启
props
的类型推导,必须使用 defineComponent()
。传入 setup()
的 props
对象类型是从 props
选项中推导而来。
import { defineComponent } from 'vue'export default defineComponent({props: {message: String},setup(props) {props.message // <-- 类型:string}
})
在 中,
emit
函数的类型标注也可以通过运行时声明或是类型声明进行:
这个类型参数应该是一个带调用签名的类型字面量。这个类型字面量的类型就是返回的 emit 函数的类型。我们可以看到,基于类型的声明使我们可以对所触发事件的类型进行更细粒度的控制。
若没有使用 ,
defineComponent()
也可以根据 emits
选项推导暴露在 setup
上下文中的 emit
函数的类型:
import { defineComponent } from 'vue'export default defineComponent({emits: ['change'],setup(props, { emit }) {emit('change') // <-- 类型检查 / 自动补全}
})
ref 会根据初始化时的值推导其类型:
import { ref } from 'vue'// 推导出的类型:Ref
const year = ref(2020)// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'
有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref 这个类型:
import { ref } from 'vue'
import type { Ref } from 'vue'const year: Ref = ref('2020')year.value = 2020 // 成功!
或者,在调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为:
// 得到的类型:Ref
const year = ref('2020')year.value = 2020 // 成功!
如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:
// 推导得到的类型:Ref
const n = ref()
reactive() 也会隐式地从它的参数中推导类型:
import { reactive } from 'vue'// 推导得到的类型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })
要显式地标注一个 reactive 变量的类型,我们可以使用接口:
import { reactive } from 'vue'interface Book {title: stringyear?: number
}const book: Book = reactive({ title: 'Vue 3 指引' })
不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
computed() 会自动从其计算函数的返回值上推导出类型:
import { ref, computed } from 'vue'const count = ref(0)// 推导得到的类型:ComputedRef
const double = computed(() => count.value * 2)// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')
你还可以通过泛型参数显式指定类型:
const double = computed(() => {// 若返回值不是 number 类型则会报错
})
在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型。让我们看一下这个例子:
没有类型标注时,这个 event 参数会隐式地标注为 any 类型。这也会在 tsconfig.json 中配置了 “strict”: true 或 “noImplicitAny”: true 时报出一个 TS 错误。因此,建议显式地为事件处理函数的参数标注类型。此外,你可能需要显式地强制转换 event 上的属性:
function handleChange(event: Event) {console.log((event.target as HTMLInputElement).value)
}
provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'const key = Symbol() as InjectionKeyprovide(key, 'foo') // 若提供的是非字符串值会导致错误const foo = inject(key) // foo 的类型:string | undefined
建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。
当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:
const foo = inject('foo') // 类型:string | undefined
注意注入的值仍然可以是 undefined,因为无法保证提供者一定会在运行时 provide 这个值。
当提供了一个默认值后,这个 undefined 类型就可以被移除:
const foo = inject('foo', 'bar') // 类型:string
如果你确定该值将始终被提供,则还可以强制转换该值:
const foo = inject('foo') as string
模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建:
注意为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。
有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 MyModal 子组件,它有一个打开模态框的方法:
为了获取 MyModal 的类型,我们首先需要通过 typeof 得到其类型,再使用 TypeScript 内置的 InstanceType 工具类型来获取其实例类型:
注意,如果你想在 TypeScript 文件而不是在 Vue SFC 中使用这种技巧,需要开启 Volar 的 Takeover 模式。
选项式 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 | undefinedthis.id // 类型:number | string | undefinedthis.msg // 类型:stringthis.metadata // 类型:any}
})
然而,这种运行时 props 选项仅支持使用构造函数来作为一个 prop 的类型——没有办法指定多层级对象或函数签名之类的复杂类型。
我们可以使用 PropType 这个工具类型来标记更复杂的 props 类型:
import { defineComponent } from 'vue'
import type { PropType } from 'vue'interface Book {title: stringauthor: stringyear: number
}export default defineComponent({props: {book: {// 提供相对 `Object` 更确定的类型type: Object as PropType,required: true},// 也可以标记函数callback: Function as PropType<(id: number) => void>},mounted() {this.book.title // stringthis.book.year // number// TS Error: argument of type 'string' is not// assignable to parameter of type 'number'this.callback?.('123')}
})
注意事项 如果你的 TypeScript 版本低于 4.7,在使用函数作为 prop 的 validator 和 default
选项值时需要格外小心——确保使用箭头函数:
import { defineComponent } from 'vue'
import type { PropType } from 'vue'interface Book {title: stringyear?: number
}export default defineComponent({props: {bookA: {type: Object as PropType,// 如果你的 TypeScript 版本低于 4.7,确保使用箭头函数default: () => ({title: 'Arrow Function Expression'}),validator: (book: Book) => !!book.title}}
})
这会防止 TypeScript 将 this 根据函数内的环境作出不符合我们期望的类型推导。这是之前版本的一个设计限制,不过现在已经在
TypeScript 4.7 中解决了。
我们可以给 emits 选项提供一个对象来声明组件所触发的事件,以及这些事件所期望的参数类型。试图触发未声明的事件会抛出一个类型错误:
import { defineComponent } from 'vue'export default defineComponent({emits: {addBook(payload: { bookName: string }) {// 执行运行时校验return payload.bookName.length > 0}},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 + '!'}},mounted() {this.greeting // 类型:string}
})
在某些场景中,你可能想要显式地标记出计算属性的类型以确保其实现是正确的:
import { defineComponent } from 'vue'export default defineComponent({data() {return {message: 'Hello!'}},computed: {// 显式标注返回类型greeting(): string {return this.message + '!'},// 标注一个可写的计算属性greetingUppercased: {get(): string {return this.greeting.toUpperCase()},set(newValue: string) {this.message = newValue.toUpperCase()}}}
})
在某些 TypeScript 因循环引用而无法推导类型的情况下,可能必须进行显式的类型标注。
在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型。让我们看一下这个例子:
没有类型标注时,这个 event 参数会隐式地标注为 any 类型。这也会在 tsconfig.json 中配置了 “strict”: true 或 “noImplicitAny”: true 时抛出一个 TS 错误。因此,建议显式地为事件处理函数的参数标注类型。此外,你可能需要显式地强制转换 event 上的属性:
import { defineComponent } from 'vue'export default defineComponent({methods: {handleChange(event: Event) {console.log((event.target as HTMLInputElement).value)}}
})
某些插件会通过 app.config.globalProperties
为所有组件都安装全局可用的属性。举例来说,我们可能为了请求数据而安装了 this.$http
,或者为了国际化而安装了 this.$translate
。为了使 TypeScript 更好地支持这个行为,Vue 暴露了一个被设计为可以通过 TypeScript 模块扩展来扩展的 ComponentCustomProperties
接口:
import axios from 'axios'declare module 'vue' {interface ComponentCustomProperties {$http: typeof axios$translate: (key: string) => string}
}
我们可以将这些类型扩展放在一个 .ts 文件,或是一个影响整个项目的 *.d.ts 文件中。无论哪一种,都应确保在 tsconfig.json 中包括了此文件。对于库或插件作者,这个文件应该在 package.json 的 types 属性中被列出。
为了利用模块扩展的优势,你需要确保将扩展的模块放在 TypeScript 模块 中。 也就是说,该文件需要包含至少一个顶级的 import 或 export,即使它只是 export {}。如果扩展被放在模块之外,它将覆盖原始类型,而不是扩展!
// 不工作,将覆盖原始类型。
declare module 'vue' {interface ComponentCustomProperties {$translate: (key: string) => string}
}
// 正常工作。
export {}declare module 'vue' {interface ComponentCustomProperties {$translate: (key: string) => string}
}
某些插件,比如 vue-router,提供了一些自定义的组件选项,比如 beforeRouteEnter:
import { defineComponent } from 'vue'export default defineComponent({beforeRouteEnter(to, from, next) {// ...}
})
如果没有确切的类型标注,这个钩子函数的参数会隐式地标注为 any 类型。我们可以为 ComponentCustomOptions 接口扩展自定义的选项来支持:
import { Route } from 'vue-router'declare module 'vue' {interface ComponentCustomOptions {beforeRouteEnter?(to: Route, from: Route, next: () => void): void}
}
现在这个 beforeRouteEnter 选项会被准确地标注类型。注意这只是一个例子——像 vue-router 这种类型完备的库应该在它们自己的类型定义中自动执行这些扩展。
这种类型扩展和全局属性扩展受到相同的限制。
如有不足,望大家多多指点! 谢谢!