一、TS 快速上手
1. 关于TS
TypeScript 是 JavaScript 的一个超集,可以编译成纯 JavaScript。TypeScript 在 JavaScript 的基础上添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
2. 基础
2.1 基本语法
:
TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。
(1) 原始值类型
let bool: boolean = false;
let num: number = 10;
let str: string = 'sip';
// var bool = false;
// var num = 10;
// var str = 'sip';
(2) 特殊类型
-
Any
任意值(Any)用来表示允许赋值为任意类型。 在任意值上访问任何属性 / 调用任何方法都是允许的
let notSure: any = 'any'; notSure = 1;
未声明类型的变量
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型: 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let something;
// 等同于 let something: any;
-
Void
void
类型像是与any
类型相反,它表示没有任何类型 ;在 TypeScript 中,可以用void
表示没有任何返回值的函数:function alertName(): void { alert('My name is Tom'); }
声明一个 void
类型的变量没有什么用,因为你只能将它赋值为 undefined
和 null
:
let unusable: void = undefined;
-
Null 和 Undefined
let u: undefined = undefined; let n: null = null;
与 void
的区别是,undefined
和 null
是所有类型的子类型。也就是说 undefined
类型的变量,可以赋值给 number
类型的变量:
// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;
而 void
类型的变量不能赋值给 number
类型的变量:
let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
2.2 类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
以下代码虽然没有指定类型,但是会在编译的时候报错:
let num = 'seven';
num = 7;
// error TS2322: Type '7' is not assignable to type 'string'.
事实上,它等价于:
let num: string = 'seven';
num = 7;
// error TS2322: Type '7' is not assignable to type 'string'.
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查:
let num;
num = 'seven';
num = 7;
2.3 联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
let strNum: string | number;
strNum = 'seven';
strNum = 7;
let strNum2: string | number;
strNum2 = true;
// error TS2322: Type 'true' is not assignable to type 'string | number'
2.4 对象的类型——接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
-
例子
// 接口 Person (接口一般首字母大写) interface Person { name: string; age: number; } let man: Person = { name: 'Tom', age: 25 };
定义的变量比接口多/少了一些属性都是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致
-
(1) 可选属性
可选属性的含义是该属性可以不存在,但不允许添加未定义的属性:
interface Person { name: string; age?: number; } let man: Person = { name: 'Tom' }; let man2: Person = { name: 'Tom', age: 25 }; let man3: Person = { name: 'Tom', tel: '1370000000' }; // error TS2322: Type '{ name: string; tel: string; }' is not assignable to type 'Person'. // Object literal may only specify known properties, and 'tel' does not exist in type 'Person'.
-
(2) 任意属性
interface Person { name: string; age?: number; [propName: string]: any; } let man: Person = { name: 'Tom', gender: 'male' };
使用
[propName: string]
定义了任意属性取string
类型的值。- 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
- 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
-
(3) 只读属性
interface Person { readonly id: number; name: string; age?: number; [propName: string]: any; } let man: Person = { id: 99999, name: 'Tom', gender: 'male' }; man.id = 10000; // error TS2540: Cannot assign to 'id' because it is a read-only property.
2.5 数组的类型
-
类型 + 方括号
数组的项中不允许出现其他的类型:
let fibonacci: number[] = [1, '1', 2, 3, 5]; // Type 'string' is not assignable to type 'number'.
-
数组泛型
let fibonacci: Array
= [1, 1, 2, 3, 5]; -
用接口表示数组
interface NumberArray { [index: number]: number; } let fibonacci: NumberArray = [1, 1, 2, 3, 5];
-
类数组
类数组(Array-like Object)不是数组类型,比如
arguments
:function sum() { let args: { [index: number]: any; length: number; callee: Function; } = arguments; }
-
any 在数组中的应用
let list: any[] = ['1', 1, { key: 'value' }];
2.6 函数的类型
-
函数声明
function sum(x: number, y: number): number { return x + y; } // 注意,输入多余的(或者少于要求的)参数,是不被允许的: sum(1, 2, 3); // error TS2554: Expected 2 arguments, but got 3. sum(1); // error TS2554: Expected 2 arguments, but got 1.
-
函数表达式
let mySum = function (x: number, y: number): number { return x + y; };
上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的
mySum
,是通过赋值操作进行类型推论而推断出来的。
如果需要我们手动给 mySum
添加类型,则应该是这样:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
注意不要混淆了 TypeScript 中的
=>
和 ES6 中的=>
。在 TypeScript 的类型定义中,
=>
用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
-
用接口定义函数的形状
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { return source.search(subString) !== -1; }
采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
-
可选参数
前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用
?
表示可选的参数:function buildName(firstName: string, lastName?: string) { if (lastName) { return firstName + ' ' + lastName; } else { return firstName; } } let tomcat = buildName('Tom', 'Cat'); let tom = buildName('Tom');
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
function buildName(firstName?: string, lastName: string) {
if (firstName) {
return firstName + ' ' + lastName;
} else {
return lastName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');
// error TS1016: A required parameter cannot follow an optional parameter.
-
参数默认值
TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制
function buildName(firstName: string = 'Tom', lastName: string) { return firstName + ' ' + lastName; } let tomcat = buildName('Tom', 'Cat'); let cat = buildName(undefined, 'Cat');
-
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
2.7 声明文件
声明文件必需以 .d.ts
为后缀。
一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。
- declare var 声明全局变量 (declare let 和 declare const)
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- interface 和 type 声明全局类型
- export 导出变量
- export namespace 导出(含有子属性的)对象
- export default ES6 默认导出
- export = commonjs 导出模块
- export as namespace UMD 库声明全局变量
- declare global 扩展全局变量
- declare module 扩展模块
- ///
三斜线指令
3. 补充
3.1 元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let strNumberList: [string, number];
strNumberList = ['hello', 10]; // OK
strNumberList = [10, 'hello']; // webstorm会报红, 编译会出错
3.2 枚举
enum
类型是对JavaScript标准数据类型的一个补充。
enum Color {Red, Green, Blue} // 默认从0开始编号
let color: Color = Color.Green;
// 编译后
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var color = Color.Green;
// enum Color {Red = 1, Green, Blue} 指定从1开始编号
// enum Color {Red = 1, Green = 2, Blue = 4} 手动赋值
3.3 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
二、TS 在 Vue2.x 的实践
1. 构建
通过官方脚手架构建安装 (Vue CLI 3)
// 1\. 如果没有安装 Vue CLI 就先安装
npm install --global @vue/cli
// 2\. 创建一个新工程,并选择 "Manually select features (手动选择特性)" 选项
vue create project-name
然后,命令行会要求选择预设。使用箭头键选择 Manually select features。
接下来,只需确保选择了 TypeScript 和 Babel 选项,然后配置其余设置,设置完成 vue cli 就会开始安装依赖并设置项目 。
[图片上传失败...(image-7844bb-1608444165739)]
2. 目录解析
安装完成打开项目,目录结构如下:
|-- project-name
|-- .browserslistrc # browserslistrc 配置文件 (用于支持 Autoprefixer)
|-- .gitignore
|-- .eslintrc.js # eslint 相关配置
|-- babel.config.js # babel-loader 配置
|-- package-lock.json
|-- package.json # package.json 依赖
|-- postcss.config.js # postcss 配置
|-- README.md
|-- tsconfig.json # typescript 配置
|-- vue.config.js # vue-cli 配置
|-- public # 静态资源 (会被直接复制)
| |-- favicon.ico # favicon图标
| |-- index.html # html模板
|-- src
| |-- App.vue # 入口页面
| |-- main.ts # 入口文件 加载组件 初始化等
| |-- shims-tsx.d.ts
| |-- shims-vue.d.ts
| |-- assets # 主题 字体等静态资源 (由 webpack 处理加载)
| |-- components # 全局组件
| |-- router # 路由
| |-- store # 全局 vuex store
| |-- styles # 全局样式
| |-- views # 所有页面
ts构建的项目目录与之前用js
构建的区别不大,区别主要是之前 js 后缀的现在改为了ts后缀,还多了tsconfig.json
、shims-tsx.d.ts
、shims-vue.d.ts
这几个文件:
-
tsconfig.json
: typescript配置文件,主要用于指定待编译的文件和定义编译选项 -
shims-tsx.d.ts
: 允许.tsx 结尾的文件,在 Vue 项目中编写 jsx 代码 -
shims-vue.d.ts
: 主要用于 TypeScript 识别.vue 文件,Ts 默认并不支持导入 vue 文件
tsconfig.json`推荐配置
// tsconfig.json
{
"compilerOptions": {
// 与 Vue 的浏览器支持保持一致
"target": "es5",
// 这可以对 `this` 上的数据 property 进行更严格的推断
"strict": true,
// 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
"module": "es2015",
"moduleResolution": "node"
}
}
注意: 需要引入 strict: true
(或者至少 noImplicitThis: true
,这是 strict
模式的一部分) 以利用组件方法中 this
的类型检查,否则它会始终被看作 any
类型。
3. 使用
3.1 在 vue 中使用 typescript 常用的几个库
-
vue-class-component
:是一个 Class Decorator,该库通过装饰器模式实现了 vue 的 ts 适配,也是官方推荐的使用 ts 方式
-
vue-property-decorator
:是在
vue-class-component
基础上进行了修改与扩充 -
vuex-module-decorators
:是在 typeScript 环境下中使用 vuex 的一种解决方案
3.2 上手
要让 TypeScript 正确推断 Vue 组件选项中的类型,我们需要使用 Vue.component
或 Vue.extend
定义组件。
3.2.1 vue-class-component
vue-class-component 对 Vue
组件进行了一层封装,让 Vue
组件语法在结合了 TypeScript
语法之后更加扁平化
3.2.2 vue-property-decorator
vue-property-decorator 是在 vue-class-component
上增强了更多的结合 Vue
特性的装饰器,新增了这 7 个装饰器:
@Emit
@Inject
@Model
@Prop
@Provide
@Watch
-
@Component
(从vue-class-component
继承)
(1) 组件声明
- 引入组件:和原生写法一致,都需要先引入再注册
(2) 响应式data 和 Prop声明
-
data
类语法中可以直接定义为类的实例属性作为组件的响应式数据
-
prop
类语法实现组件 props 定义是通过装饰器
@Prop
实现
等同于
总结: 对于data里的变量对顶,我们可以直接按ts定义类变量的写法写就可以
那么如果是计算属性呢? 这就要用到getter了.
等同于
总结: 对于Vue中的计算属性,我们只需要将该计算属性名定义为一个函数,并在函数前加上get关键字即可.
原本Vue中的computed里的每个计算属性都变成了在前缀添加get的函数.
@Emit
关于Vue中的事件的监听与触发,Vue提供了两个函数on.那么在vue-property-decorator中如何使用呢?
这就需要用到vue-property-decorator提供的@Emit属性.
运行上面的代码会打印 'hello' 'world', 为什么呢? 让我们来看看它等同于什么
可以看到,在@Emit装饰器的函数会在运行之后触发等同于其函数名(驼峰式会转为横杠式写法)的事件, 并将其函数传递给$emit.
如果我们想触发特定的事件呢,比如在emitTodo下触发reset事件:
我们只需要给装饰器@Emit传递一个事件名参数reset,这样函数emitTodo运行之后就会触发reset事件.
总结:在Vue中我们是使用$emit触发事件,使用vue-property-decorator时,可以借助@Emit装饰器来实现.@Emit修饰的函数所接受的参数会在运行之后触发事件的时候传递过去.
@Emit触发事件有两种写法
@Emit()不传参数,那么它触发的事件名就是它所修饰的函数名.
@Emit(name: string),里面传递一个字符串,该字符串为要触发的事件名.
@Watch
我们可以利用vue-property-decorator提供的@Watch装饰器来替换Vue中的watch属性,以此来监听值的变化.
在Vue中监听器的使用如下:
export default{
watch: {
'child': this.onChangeValue
// 这种写法默认 immediate
和deep
为false
,
'person': {
handler: 'onChangeValue',
immediate: true,
deep: true
}
},
methods: {
onChangeValue(newVal, oldVal){
// todo...
}
}
}
那么我们如何使用@Watch装饰器来改造它呢?
import {Vue, Component, Watch} from 'vue-property-decorator';
@Watch('child')
onChangeValue(newVal: string, oldVal: string){
// todo...
}
@Watch('person', {immediate: true, deep: true})
onChangeValue(newVal: Person, oldVal: Person){
// todo...
}
总结: @Watch使用非常简单,接受第一个参数为要监听的属性名 第二个属性为可选对象.@Watch所装饰的函数即监听到属性变化之后的操作.
@Prop
我们在使用Vue时有时会遇到子组件接收父组件传递来的参数.我们需要定义Prop属性.
比如子组件从父组件接收三个属性propA,propB,propC.
propA类型为Number
propB默认值为default value
propC类型为String或者Boolean
export default {
props: {
propA: {
type: Number
},
propB: {
default: 'default value'
},
propC: {
type: [String, Boolean]
},
}
}
我们使用vue-property-decorator提供的@Prop可以将上面的代码改造为如下:
这里 !和可选参数?是相反的, !告诉TypeScript我这里一定有值.
总结: @Prop接受一个参数可以是类型变量或者对象或者数组.@Prop接受的类型比如Number是JavaScript的类型,之后定义的属性类型则是TypeScript的类型.
Mixins
在使用Vue进行开发时我们经常要用到混合,结合TypeScript之后我们有两种mixins的方法.
一种是vue-class-component提供的.
//定义要混合的类 mixins.ts
import Vue from 'vue';
import Component from 'vue-class-component';
@Component // 一定要用Component修饰
export default class myMixins extends Vue {
value: string = "Hello"
}
// 引入
import Component {mixins} from 'vue-class-component';
import myMixins from 'mixins.ts';
@Component
export class myComponent extends mixins(myMixins) {
// 直接extends myMinxins 也可以正常运行
created(){
console.log(this.value) // => Hello
}
}
第二种方式是在@Component中混入.
我们改造一下mixins.ts,定义vue/type/vue模块,实现Vue接口
// mixins.ts
import { Vue, Component } from 'vue-property-decorator';
declare module 'vue/types/vue' {
interface Vue {
value: string;
}
}
@Component
export default class myMixins extends Vue {
value: string = 'Hello'
}
混入
import { Vue, Component, Prop } from 'vue-property-decorator';
import myMixins from '@static/js/mixins';
@Component({
mixins: [myMixins]
})
export default class myComponent extends Vue{
created(){
console.log(this.value) // => Hello
}
}
总结: 两种方式不同的是在定义mixins时如果没有定义vue/type/vue模块, 那么在混入的时候就要继承该mixins; 如果定义vue/type/vue模块,在混入时可以在@Component中mixins直接混入.
@Model
Vue组件提供model: {prop?: string, event?: string}让我们可以定制prop和event.
默认情况下,一个组件上的v-model 会把 value用作 prop且把 input用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop来达到不同的目的。使用model选项可以回避这些情况产生的冲突。
下面是Vue官网的例子
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
// this allows using the value
prop for a different purpose
value: String,
// use checked
as the prop which take the place of value
checked: {
type: Number,
default: 0
}
},
// ...
})
上述代码相当于:
:checked="foo"
@change="val => { foo = val }"
value="some value">
即foo双向绑定的是组件的checke, 触发双向绑定数值的事件是change
使用vue-property-decorator提供的@Model改造上面的例子.
import { Vue, Component, Model} from 'vue-property-decorator';
@Component
export class myCheck extends Vue{
@Model ('change', {type: Boolean}) checked!: boolean;
}
总结, @Model()接收两个参数, 第一个是event值, 第二个是prop的类型说明, 与@Prop类似, 这里的类型要用JS的. 后面在接着是prop和在TS下的类型说明.
链接:https://www.jianshu.com/p/d8ed3aa76e9b