# 在 Vue 中使用 Typescript

随着应用的增长,静态类型系统可以帮助防止许多潜在的运行时错误,这就是为什么 Vue 3 是用 TypeScript 编写的。这意味着在 Vue 中使用 TypeScript 不需要任何其他工具——它具有一等公民支持。

# 推荐配置

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    // 这样就可以对 `this` 上的数据属性进行更严格的推断
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node"
  }
}
1
2
3
4
5
6
7
8
9
10

请注意,必须包含 strict: true (或至少包含 noImplicitThis: true,它是 strict 标志的一部分) 才能在组件方法中利用 this 的类型检查,否则它总是被视为 any 类型。

查看 Typescript 编译选项文档 (opens new window) 获取更多选项

# Webpack 配置

如果使用自定义的 webpack 配置,需要配置 ts-loader 来解析 Vue 单文件组件中的 <script lang='ts'></script>

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      // 配置 ts-loader
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
        },
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      }
      // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 定义 Vue 组件

要想让 Typescript 推断出 props 的属性类型,可以使用 defineComponent 的方式来定义一个组件

import { defineComponent } from 'vue'
const component = defineComponent({
  // 已启用类型推断
})
1
2
3
4

如果使用的是单文件组件,你可以写成:

<script lang="ts">
  import { defineComponent } from 'vue'
  export default defineComponent({
    // 已启用类型推断
  })
</script>
1
2
3
4
5
6

# 与 options API 一起使用

TypeScript 应该能够在不显式定义类型的情况下推断大多数类型。例如,对于拥有一个数字类型的 count property 的组件来说,如果你试图对其调用字符串独有的方法,会出现错误:

const Component = defineComponent({
  data() {
    return {
      count: 0,
    }
  },
  mounted() {
    const result = this.count.split('') // => 会报错
  },
})
1
2
3
4
5
6
7
8
9
10

如果你有一个复杂的类型或接口,你可以使用 type assertion (类型断言) 对其进行指明:

interface Book {
  title: string
  author: string
  year: number
}

const Component = defineComponent({
  data() {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: 2020,
      } as Book,
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#globalProperties 扩充类型

Vue 3 提供了一个 globalProperties 对象,用于储存全局可用的属性。例如一个插件需要全局注入某些属性:

// 用户定义
import axios from 'axios'
const app = Vue.createApp({})
app.config.globalProperties.$http = axios
// 验证数据的插件
export default {
  install(app, options) {
    app.config.globalProperties.$validate = (data: object, rule: object) => {
      // 检查对象是否合规
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12

为了告诉 TypeScript 这些新 property,我们可以使用模块扩充 (module augmentation)

在上述示例中,我们可以添加以下类型声明:

import axios from 'axios'
declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $http: typeof axios
    $validate: (data: object, rule: object) => boolean
  }
}
1
2
3
4
5
6
7

我们可以把这些类型声明放在同一个文件里,或一个项目级别的 *.d.ts 文件 (例如在 TypeScript 会自动加载的 src/typings 文件夹中)。对于库/插件作者来说,这个文件应该被定义在 package.json 的 types property 里。

注意

确认类型声明文件是一个 Typescript 模块 为了利用好模块扩充,你需要确认你的文件中至少有一个顶级的 importexport,哪怕只是一个 export {}。 在 TypeScript 中,任何包含一个顶级 import 或 export 的文件都被视为一个“模块”。如果类型声明在模块之外,该声明会覆盖而不是扩充原本的类型。

关于 ComponentCustomProperties 类型的更多信息,请参阅其 在 @vue/runtime-core 中的定义 (opens new window)其 TypeScript 测试用例 (opens new window) 学习更多。

# 注解返回类型

由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注解计算属性的返回类型。

import { defineComponent } from 'vue'

const Component = defineComponent({
  data() {
    return {
      message: 'Hello!',
    }
  },
  computed: {
    // 需要注解
    greeting(): string {
      return this.message + '!'
    },

    // 在使用 setter 进行计算时,需要对 getter 进行注解
    greetingUppercased: {
      // 需要注解返回值类型与参数类型
      get(): string {
        return this.greeting.toUpperCase()
      },
      set(newValue: string) {
        this.message = newValue.toUpperCase()
      },
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 注解 props

Vue 对定义了 type 的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用 PropType 指明构造函数:

import { defineComponent, PropType } from 'vue'

interface Book {
  title: string
  author: string
  year: number
}

const Component = defineComponent({
  props: {
    name: String,
    id: [Number, String],
    success: { type: String },
    callback: {
      type: Function as PropType<() => void>,
    },
    book: {
      type: Object as PropType<Book>,
      required: true,
    },
    metadata: {
      type: null, // metadata 的类型是 any
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

注意

由于 TypeScript 中的 设计限制 (opens new window),当它涉及到为了对函数表达式进行类型推理,你必须注意对象和数组的 validatordefault 值:

import { defineComponent, PropType } from 'vue'

interface Book {
  title: string
  year?: number
}

const Component = defineComponent({
  props: {
    bookA: {
      type: Object as PropType<Book>,
      // 请务必使用箭头函数
      default: () => ({
        title: 'Arrow Function Expression',
      }),
      validator: (book: Book) => !!book.title,
    },
    bookB: {
      type: Object as PropType<Book>,
      // 或者提供一个明确的 this 参数
      default(this: void) {
        return {
          title: 'Function Expression',
        }
      },
      validator(this: void, book: Book) {
        return !!book.title
      },
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 注解 emit

我们可以为触发的事件注解一个有效载荷。另外,所有未声明的触发事件在调用时都会抛出一个类型错误。

const Component = defineComponent({
  emits: {
    addBook(payload: { bookName: string }) {
      // perform runtime 验证
      return payload.bookName.length > 0
    },
  },
  methods: {
    onSubmit() {
      this.$emit('addBook', {
        bookName: 123, // 类型错误!
      })
      this.$emit('non-declared-event') // 类型错误!
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 与组合式 API 一起使用

setup() 函数中,不需要将类型传递给 props 参数,因为它将从 props 组件选项推断类型

import { defineComponent } from 'vue'

const Component = defineComponent({
  props: {
    message: {
      type: String,
      required: true,
    },
  },

  setup(props) {
    // 正确, 'message' 被声明为字符串
    const result = props.message.split('')
    // 将引发错误: Property 'filter' does not exist on type 'string'
    const filtered = props.message.filter(p => p.value)
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 类型声明 refs

refs 根据初始值推断类型:

import { defineComponent, ref } from 'vue'

const Component = defineComponent({
  setup() {
    const year = ref(2020)
    // 自动推断 year 是 number 类型,没有 .split 函数就会报错
    const result = year.value.split('')
  },
})
1
2
3
4
5
6
7
8
9

有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数

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

year.value = 2020 // 可以是 string 或者 number
1
2
3

TIP

如果类型未知,建议 ref 改为 Ref<T>

# 为模板引用定义类型

有时你可能需要为一个子组件标注一个模板引用,以调用其公共方法。例如我们有一个 MyModal 子组件,它有一个打开模态的方法:

import { defineComponent, ref } from 'vue'
const MyModal = defineComponent({
  setup() {
    const isContentShown = ref(false)
    const open = () => (isContentShown.value = true)
    return {
      isContentShown,
      open,
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11

我们希望从其父组件的一个模板引用调用这个方法:

import { defineComponent, ref } from 'vue'
const MyModal = defineComponent({
  setup() {
    const isContentShown = ref(false)
    const open = () => (isContentShown.value = true)
    return {
      isContentShown,
      open,
    }
  },
})
const app = defineComponent({
  components: {
    MyModal,
  },
  template: `
    <button @click="openModal">Open from parent</button>
    <my-modal ref="modal" />
  `,
  setup() {
    const modal = ref()
    const openModal = () => {
      modal.value.open()
    }
    return { modal, openModal }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

上述代码没问题,但是没有关于 MyModal 及其可用方法的类型信息。为了解决这个问题,你应该在创建引用时使用 InstanceType

setup() {
  const modal = ref<InstanceType<typeof MyModal>>()
  const openModal = () => {
    modal.value?.open()
  }
  return { modal, openModal }
}
1
2
3
4
5
6
7

请注意你还需要使用 可选链操作符 (opens new window) 或其它方式来确认 modal.value 不是 undefined

# 类型声明 reactive

当声明类型 reactive property,我们可以使用接口:

import { defineComponent, reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

export default defineComponent({
  name: 'HelloWorld',
  setup() {
    const book = reactive<Book>({ title: 'Vue 3 Guide' })
    // or
    const book: Book = reactive({ title: 'Vue 3 Guide' })
    // or
    const book = reactive({ title: 'Vue 3 Guide' }) as Book
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 类型声明 computed

计算值将根据返回值自动推断类型

import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  name: 'CounterButton',
  setup() {
    let count = ref(0)

    // 只读 (之前说过 computed 返回一个只读的 ref)
    const doubleCount = computed(() => count.value * 2)

    const result = doubleCount.value.split('') // => Property 'split' does not exist on type 'number'
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 为事件处理器添加类型

在处理原生 DOM 事件的时候,正确地为处理函数的参数添加类型或许会是有用的。让我们看这个例子:

<template>
  <input type="text" @change="handleChange" />
</template>
<script lang="ts">
  import { defineComponent } from 'vue'
  export default defineComponent({
    setup() {
      // `evt` 将会是 `any` 类型
      const handleChange = evt => {
        console.log(evt.target.value) // 此处 TS 将抛出异常
      }
      return { handleChange }
    },
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如你所见,在没有为 evt 参数正确地声明类型的情况下,当我们尝试获取 <input> 元素的值时,TypeScript 将抛出异常。解决方案是将事件的目标转换为正确的类型:

const handleChange = (evt: Event) => {
  console.log((evt.target as HTMLInputElement).value)
}
1
2
3