依据 《2022 前端开发市场状态调查报告》数据显示,应用 TypeScript 的人数曾经达到 84%,和 2021 年相比减少了 7 个百分点。
TypeScript 堪称逐年炽热,使用者出现逐年回升的趋势,再不学起来就说不过去。
近期OpenTiny做了一次大的降级,将原来运行了 9年
的 JavaScript
代码降级到了 TypeScript
,并通过 Monorepo 进行子包的治理,还在用 JavaScript 的敌人能够放松降级了,这边特意筹备了一份《JS我的项目革新TS指南》文档供大家参考,顺便介绍了一些 TS 基础知识和 TS 在 Vue 中的一些实际。
通过本文你将播种:
- 通过理解 TS 的四大益处,压服本人下定决心学习 TS
- 5 分钟学习 TS 最根底和罕用的知识点,疾速入门,包教包会
- 理解如何在 Vue 中应用 TypeScript,给 Vue2 开发者切换到 Vue3 + TypeScript 提供最根本的参考
- 如何将现有的 JS 我的项目革新成 TS 我的项目
1 学习 TS 的益处
1.1 益处一:紧跟潮流:让本人看起来很酷
如果你没学过 TS
你的前端敌人:都 2023 年了,你还不会 TS?给你一个眼色你本人感悟吧
如果你学过 TS
你的前端敌人:哇,你们的我的项目曾经用上 Vue3 + TS 啦,看起来真棒!教教我吧
如果说下面那个益处太虚了,那上面的3条益处可都是实实在在能让本人受害的。
1.2 益处二:智能提醒:晋升开发者体验和效率
当循环一个对象数组时,对象的属性列表能够间接显示进去,不必到对象的定义中去查问该对象有哪些属性。
通过调用后盾接口获取的异步数据也能够通过TS类型进行智能提醒,这样相当于集成了接口文档,后续后盾批改字段,咱们很容易就能发现。
Vue 组件的属性和事件都能够智能提醒。
下图是 OpenTiny 跨端跨框架前端组件库中的 Alert 组件,当在组件标签中输出 des
时,会主动提醒 description
属性;当输出 @c
时,会主动提醒 @close
事件。
1.3 益处三:谬误标记:代码哪里有问题一眼就晓得
在 JS 我的项目应用不存在的对象属性,在编码阶段不容易看进去,到运行时才会报错。
在 TS 我的项目应用不存在的对象属性,在IDE中会有红色波浪线标记,鼠标移上去能看到具体的错误信息。
在 JS 我的项目,调用办法时拼错单词不容易被发现,要在运行时才会将谬误裸露进去。
在 TS 我的项目会有红色波浪线提醒,一眼就看出拼错单词。
1.4 益处四:类型束缚:用我的代码就得听我的
你写了一个工具函数 getType 给他人用,限定参数只能是指定的字符串,这时如果应用这个函数的人传入其余字符串,就会有红色波浪线提醒。
Vue 组件也是一样的,能够限定组件 props 的类型,组件的使用者如果传入不正确的类型,将会有谬误提醒,比方: OpenTiny 的 Alert 组件,closable 只能传入 Boolean 值,如果传入一个字符串就会有谬误提醒。
2 极简 TS 根底,5分钟学会
以下内容尽管不多,但蕴含了理论我的项目开发中最实用的局部,对于 TS 入门者来说也是能很快学会的,基本上是有手就会写。
2.1 根本类型
用得较多的类型就上面5个,更多类型请参考:TS官网文档
- 布尔 boolean
- 数值 number
- 字符串 string
- 空值 void:示意没有任何返回值的函数
- 任意 any:示意不被类型查看
用法也很简略:
let isDone: boolean = false;let myFavoriteNumber: number = 6;let myName: string = 'Kagol';function alertName(name: string): void { console.log(`My name is ${name}`); }
默认状况下,name 会主动类型推导成 string 类型,此时如果给它赋值为一个 number 类型的值,会呈现谬误提醒。
let name = 'Kagol' name = 6
如果给 name 设置 any 类型,示意不做类型查看,这时谬误提醒隐没。
let name: any = 'Kagol' name = 6
2.2 函数
次要定义函数参数和返回值类型。
看一下例子:
const sum = (x: number, y: number): number => { return x + y }
以上代码蕴含以下 TS 校验规定:
- 调用 sum 函数时,必须传入两个参数,多一个或者少一个都不行
- 并且这两个参数的类型要为 number 类型
- 且函数的返回值为 number 类型
少参数:
多参数:
参数类型谬误:
返回值:
用问号 ?
能够示意该参数是可选的。
const sum = (x: number, y?: number): number => { return x + (y || 0); } sum(1)
如果将 y 定义为可选参数,则调用 sum 函数时能够只传入一个参数。
须要留神的是,可选参数必须接在必须参数前面。换句话说,可选参数前面不容许再呈现必须参数了。
给 y 减少默认值 0 之后,y 会主动类型推导成 number 类型,不须要加 number 类型,并且因为有默认值,也不须要加可选参数。
const sum = (x: number, y = 0): number => { return x + y }sum(1) sum(1, 2)
2.3 数组
数组类型有两种示意形式:
类型 + 方括号
表示法泛型
表示法
// `类型 + 方括号` 表示法let fibonacci: number[] = [1, 1, 2, 3, 5]// 泛型表示法let fibonacci: Array<number> = [1, 1, 2, 3, 5]
这两种都能够示意数组类型,看本人爱好进行抉择即可。
如果是类数组,则不能够用数组的形式定义类型,因为它不是真的数组,须要用 interface 进行定义
interface IArguments { [index: number]: any; length: number; callee: Function;}function sum() { let args: IArguments = arguments}
IArguments
类型已在 TypeScript 中内置,相似的还有很多:
let body: HTMLElement = document.body;let allDiv: NodeList = document.querySelectorAll('div');document.addEventListener('click', function(e: MouseEvent) { // Do something});
如果数组里的元素类型并不都是雷同的怎么办呢?
这时 any 类型就发挥作用啦啦
let list: any[] = ['OpenTiny', 112, { website: 'https://opentiny.design/' }];
2.4 接口
接口简略了解就是一个对象的“轮廓”
interface IResourceItem { name: string; value?: string | number; total?: number; checked?: boolean;}
接口是能够继承接口的
interface IClosableResourceItem extends IResourceItem { closable?: boolean;}
这样 IClosableResourceItem 就蕴含了 IResourceItem 属性和本人的 closable 可选属性。
接口也是能够被类实现的
interface Alarm { alert(): void;}class Door {}class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert') }}
如果类实现了一个接口,却不写具体的实现代码,则会有谬误提醒
2.5 联结类型 & 类型别名
联结类型是指取值能够为多种类型中的一种,而类型别名罕用于联结类型。
看以下例子:
// 联结类型let myFavoriteNumber: string | numbermyFavoriteNumber = 'six'myFavoriteNumber = 6// 类型别名type FavoriteNumber = string | numberlet myFavoriteNumber: FavoriteNumber
当 TypeScript 不确定一个联结类型的变量到底是哪个类型的时候,咱们只能拜访此联结类型的所有类型里共有的属性或办法:
function getLength(something: string | number): number { return something.length}// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.// Property 'length' does not exist on type 'number'.
上例中,length 不是 string 和 number 的共有属性,所以会报错。
拜访 string 和 number 的共有属性是没问题的:
function getString(something: string | number): string { return something.toString()}
2.6 类型断言
类型断言(Type Assertion)能够用来手动指定一个值的类型。
语法:值 as 类型,比方:(animal as Fish).swim()
类型断言次要有以下用处:
- 将一个联结类型断言为其中一个类型
- 将一个父类断言为更加具体的子类
- 将任何一个类型断言为 any
- 将 any 断言为一个具体的类型
咱们一个个来看。
用处1:将一个联结类型断言为其中一个类型
interface Cat { name: string; run(): void;}interface Fish { name: string; swim(): void;}const animal: Cat | Fish = new Animal()animal.swim()
animal 是一个联结类型,可能是猫 Cat,也可能是鱼 Fish,如果间接调用 swim 办法是要呈现谬误提醒的,因为猫不会游泳。
这时类型断言就派上用场啦啦,因为调用的是 swim 办法,那必定是鱼,所以间接断言为 Fish 就不会呈现谬误提醒。
const animal: Cat | Fish = new Animal()(animal as Fish).swim()
用处2:将一个父类断言为更加具体的子类
class ApiError extends Error { code: number = 0;}class HttpError extends Error { statusCode: number = 200;}function isApiError(error: Error) { if (typeof (error as ApiError).code === 'number') { return true; } return false;}
ApiError 和 HttpError 都继承自 Error 父类,error 变量的类型是 Error,去取 code 变量必定是不行,因为取的是 code 变量,咱们能够间接断言为 ApiError 类型。
用处3:将任何一个类型断言为 any
这个十分有用,看一下例子:
function getCacheData(key: string): any { return (window as any).cache[key];}interface Cat { name: string; run(): void;}const tom = getCacheData('tom') as Cat;
getCacheData 是一个历史遗留函数,不是你写的,因为他返回 any 类型,就等于放弃了 TS 的类型测验,如果 tom 是一只猫,外面有 name 属性和 run()
办法,但因为返回 any 类型,tom.
是没有任何提醒的。
如果将其断言为 Cat 类型,就能够 点
出 name 属性和 run()
办法。
用处4:将 any 断言为一个具体的类型
这个比拟常见的场景是给 window 挂在一个本人的变量和办法。
window.foo = 1;// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.(window as any).foo = 1;
因为 window 下没有 foo 变量,间接赋值会有谬误提醒,将 window 断言为 any 就没问题啦啦。
2.7 元组
数组合并了雷同类型的对象,而元组(Tuple)合并了不同类型的对象。
let tom: [string, number] = ['Tom', 25];
给元组类型赋值时,数组每一项的类型须要和元组定义的类型对应上。
当赋值或拜访一个已知索引的元素时,会失去正确的类型:
let tom: [string, number];tom[0] = 'Tom';tom[1] = 25;tom[0].slice(1);tom[1].toFixed(2);
也能够只赋值其中一项:
let tom: [string, number];tom[0] = 'Tom';
然而当间接对元组类型的变量进行初始化或者赋值的时候,须要提供所有元组类型中指定的项。
let tom: [string, number];tom = ['Tom'];// Property '1' is missing in type '[string]' but required in type '[string, number]'.
当增加越界的元素时,它的类型会被限度为元组中每个类型的联结类型:
let tom: [string, number];tom = ['Tom', 25];tom.push('male');tom.push(true);// Argument of type 'true' is not assignable to parameter of type 'string | number'.
push 字符串和数字都能够,布尔就不行。
2.8 枚举
枚举(Enum)类型用于取值被限定在肯定范畴内的场景,比方一周只能有七天,色彩限定为红绿蓝等。
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
console.log(Days.Sun === 0) // trueconsole.log(Days[0] === 'Sun') // trueconsole.log('Days', Days)
手动赋值:未手动赋值的枚举项会接着上一个枚举项递增。
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}
2.9 类
给类加上 TypeScript 的类型很简略,与接口相似:
class Animal { name: string constructor(name: string) { this.name = name } sayHi(welcome: string): string { return `${welcome} My name is ${this.name}` }}
类的语法波及到较多概念,请参考:
- https://es6.ruanyifeng.com/#docs/class
- https://ts.xcatliu.com/advanced/class.html
2.10 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在应用的时候再指定类型的一种个性。
能够简略了解为定义函数时的形参。
构想以下场景,咱们有一个 print 函数,输出什么,原样打印,函数的入参和返回值类型是统一的。
一开始只须要打印字符串:
function print(arg: string): string { return arg}
前面需要变了,除了能打印字符串,还要能打印数字:
function print(arg: string | number): string | number { return arg}
如果需要又变了,要打印布尔值、对象、数组,甚至自定义的类型,怎么办,写一串联结类型?显然是不可取的,用 any?那就失去了 TS 类型校验能力,沦为 JS。
function print(arg: any): any { return arg }
解决这个问题的完满办法就是泛型!
print 前面加上一对尖括号,外面写一个 T,这个 T 就相似是一个类型的形参。
这个类型形参能够在函数入参里用,也能够在函数返回值应用,甚至也能够在函数体外面的变量、函数外面用。
function print<T>(arg: T): T { return arg}
那么实参哪里来?用的时候传进来!
const res = print<number>(123)
咱们还能够应用泛型来束缚后端接口参数类型。
import axios from 'axios'interface API { '/book/detail': { id: number, }, '/book/comment': { id: number comment: string } ...}function request<T extends keyof API>(url: T, obj: API[T]) { return axios.post(url, obj)}request('/book/comment', { id: 1, comment: '十分棒!'})
以上代码对接口进行了束缚:
- url 只能是 API 中定义过的,其余 url 都会提醒谬误
- 接口参数 obj 必须和 url 能对应上,不能少属性,属性类型也不能错
而且调用 request 办法时,也会提醒 url 能够抉择哪些
如果后盾改了接口参数名,咱们一眼就看进去了,都不必去找接口文档,是不是很厉害!
泛型的例子参考了前端阿林的文章:
- 轻松拿下 TS 泛型
3 TS 在 Vue 中的实际
3.1 定义组件 props 的类型
不应用 setup 语法糖
export default defineComponent({ props: { items: { type: Object as PropType<IResourceItem[]>, default() { return [] } }, span: { type: Number, default: 4 }, gap: { type: [String, Number] as PropType<string | number>, default: '12px' }, block: { type: Object as PropType<Component>, default: TvpBlock }, beforeClose: Function as PropType<() => boolean> }})
应用 setup 语法糖 – runtime 申明
import { PropType, Component } from 'vue'const props = defineProps({ items: { type: Object as PropType<IResourceItem[]>, default() { return [] } }, span: { type: Number, default: 4 }, gap: { type: [String, Number] as PropType<string | number>, default: '12px' }, block: { type: Object as PropType<Component>, default: TvpBlock }, beforeClose: Function as PropType<() => boolean>})
应用 setup 语法糖 – type-based 申明
import { Component, withDefaults } from 'vue'interface Props { items: IResourceItem[] span: number gap: string | number block: Component beforeClose: () => void}const props = withDefaults(defineProps<Props>(), { items: () => [], span: 4, gap: '12px', block: TvpBlock})
IResourceItem:
interface IResourceItem { name: string; value?: string | number; total?: number; checked?: boolean; closable?: boolean;}
3.2 定义 emits 类型
不应用 setup 语法糖
export default defineComponent({ emits: ['change', 'update'], setup(props, { emit }) { emit('change') }})
应用 setup 语法糖
<script setup lang="ts">// runtimeconst emit = defineEmits(['change', 'update'])// type-basedconst emit = defineEmits<{ (e: 'change', id: number): void (e: 'update', value: string): void}>()</script>
3.3 定义 ref 类型
默认会主动进行类型推导
import { ref } from 'vue'// inferred type: Ref<number>const year = ref(2020)// => TS Error: Type 'string' is not assignable to type 'number'.year.value = '2020'
两种申明 ref 类型的办法
import { ref } from 'vue'import type { Ref } from 'vue'// 形式一const year: Ref<string | number> = ref('2020')year.value = 2020 // ok!// 形式二// resulting type: Ref<string | number>const year = ref<string | number>('2020')year.value = 2020 // ok!
3.4 定义 reactive 类型
默认会主动进行类型推导
import { reactive } from 'vue'// inferred type: { title: string }const book = reactive({ title: 'Vue 3 Guide' })
应用接口定义明确的类型
import { reactive } from 'vue'interface Book { title: string year?: number}const book: Book = reactive({ title: 'Vue 3 Guide' })
3.5 定义 computed 类型
默认会主动进行类型推导
import { ref, computed } from 'vue'const count = ref(0)// inferred type: ComputedRef<number>const double = computed(() => count.value * 2)// => TS Error: Property 'split' does not exist on type 'number'const result = double.value.split('')
两种申明 computed 类型的办法
import { ComputedRef, computed } from 'vue'const double: ComputedRef<number> = computed(() => { // type error if this doesn't return a number})const double = computed<number>(() => { // type error if this doesn't return a number})
3.6 定义 provide/inject 类型
provide
import { provide, inject } from 'vue'import type { InjectionKey } from 'vue'// 申明 provide 的值为 string 类型const key = Symbol() as InjectionKey<string>provide(key, 'foo') // providing non-string value will result in error
inject
// 主动推导为 string 类型const foo = inject(key) // type of foo: string | undefined// 明确指定为 string 类型const foo = inject<string>('foo') // type: string | undefined// 减少默认值const foo = inject<string>('foo', 'bar') // type: string// 类型断言为 stringconst foo = inject('foo') as string
3.7 定义模板援用的类型
<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>
3.8 定义组件模板援用的类型
定义一个 MyModal 组件
<!-- MyModal.vue --><script setup lang="ts">import { ref } from 'vue'const isContentShown = ref(false)const open = () => (isContentShown.value = true)defineExpose({ open})</script>
在 App.vue 中援用 MyModal 组件
<!-- 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>
参考 Vue 官网文档:
- TypeScript with Composition API
4 JS 我的项目转 TS
还是应用 JS 的同学有福啦!为了让大家疾速用上 TS,享受 TS 的丝滑体验,这里整顿了一份《JS 我的项目革新成 TS 我的项目指南》
。有了这份步骤指南,JS 我的项目转 TS 不再是难事!
新开源的 TinyVue 组件库,就应用这份《JS 我的项目革新成 TS 我的项目指南》
,胜利地由 JS 我的项目革新成了 TS 我的项目,悄悄地通知大家:
- TinyVue 是一套跨端、跨框架的企业级 UI 组件库,反对 Vue 2 和 Vue 3,反对 PC 端和挪动端。
- 在外部通过9年继续打磨,服务于华为内外部上千个我的项目。
- 目前代码量超过
10万行
。
这么宏大的代码量都能从 JS 转 TS,其余小规模的我的项目更是不在话下。
话不多说,大家有须要的,可间接拿走!
《JS 我的项目革新成 TS 我的项目指南》
JS 我的项目革新成 TS 步骤:
- 装置 TS:
npm i typescript ts-loader -D
- 减少 TS 配置文件:
tsconfig.json
- 批改文件后缀名:
x.js -> x.ts
x.vue
文件减少 lang:<script lang="ts">
vite.config.js
配置后缀名- 降级依赖,批改本地启动和构建脚本
- 增加
loader
/plugin
等 - 逐渐补充类型申明
tsconfig.ts
{ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true }, "include": [ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue" ]}
配置文件后缀名,减少 .ts
和 .tsx
extensions: ['.js', '.vue', '.json', '.ts', 'tsx'],
入口文件要由 main.js
改成 main.ts
entry: { app: './src/main.ts'},
须要配置下 loader
{ test: /\.tsx?$/, loader: 'ts-loader', exclude: /node_modules/, options: { appendTsSuffixTo: [/\.vue$/] }, include: [resolve('src')]}
以及 plugin
const { VueLoaderPlugin } = require('vue-loader')plugins: [ new VueLoaderPlugin()],
实现之后,先测试下我的项目是否能失常启动和构建:npm run dev
/ npm run build
都没问题之后,本次 TS 我的项目革新就实现大部分啦啦!
后续就是逐渐补充代码波及到的变量和函数的类型申明即可。
革新过程中遇到问题欢送留言探讨,心愿你也能尽快享受 TS 的丝滑开发者体验!
TinyVue 招募贡献者啦
如果你对跨端跨框架组件库 TinyVue 感兴趣,欢送参加到OpenTiny的开源社区中来,一起将它建设得更好!
参加 TinyVue 组件库建设,你将播种:
间接的价值:
- 通过打造一个跨端、跨框架的组件库我的项目,学习最新的
Monorepo
+Vite
+Vue3
+TypeScript
技术 - 学习从 0 到 1 搭建一个本人的组件库的整套流程和方法论,包含组件库工程化、组件的设计和开发等
- 为本人的简历和职业生涯添彩,参加过优良的开源我的项目,这自身就是受面试官青眼的亮点
- 结识一群优良的、酷爱学习、酷爱开源的小伙伴,大家一起打造一个平凡的产品
久远的价值:
- 打造集体品牌,晋升集体影响力
- 造就良好的编码习惯
- 取得华为云 OpenTiny 开源社区的荣誉&认可和定制小礼物
- 成为 PMC & Committer 之后还能参加 OpenTiny 整个开源生态的决策和长远规划,造就本人的治理和布局能力 将来有更多机会和可能
分割咱们
如果你对咱们 OpenTiny 的开源我的项目感兴趣,欢送增加小助手微信:opentiny-official,拉你进群,一起交换前端技术,一起玩开源。
OpenTiny 官网:https://opentiny.design/
OpenTiny 仓库:https://github.com/opentiny/
TinyVue 组件库:https://github.com/opentiny/tiny-vue
TinyNG 组件库:https://github.com/opentiny/ng