一、TS 快速上手
从 JavaScript 程序员的角度总结思考,快速上手理解 TypeScript。
1. 关于TS
TypeScript 是 JavaScript 的一个超集,可以编译成纯 JavaScript。TypeScript 在 JavaScript 的基础上添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
2. 基础
2.1 基本语法
1 | :<TypeAnnotation> |
TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。
(1) 原始值类型
1 | let bool: boolean = false; |
(2) 特殊类型
Any
任意值(Any)用来表示允许赋值为任意类型。 在任意值上访问任何属性 / 调用任何方法都是允许的
1
2let notSure: any = 'any';
notSure = 1;未声明类型的变量
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型: 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
1
2let something;
// 等同于 let something: any;Void
void类型像是与any类型相反,它表示没有任何类型 ;在 TypeScript 中,可以用void表示没有任何返回值的函数:1
2
3function alertName(): void {
alert('My name is Tom');
}声明一个
void类型的变量没有什么用,因为你只能将它赋值为undefined和null:1
let unusable: void = undefined;
Null 和 Undefined
1
2let u: undefined = undefined;
let n: null = null;与
void的区别是,undefined和null是所有类型的子类型。也就是说undefined类型的变量,可以赋值给number类型的变量:1
2
3
4
5// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;而
void类型的变量不能赋值给number类型的变量:1
2
3
4let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
2.2 类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
以下代码虽然没有指定类型,但是会在编译的时候报错:
1 | let num = 'seven'; |
事实上,它等价于:
1 | let num: string = 'seven'; |
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
1 | let num; |
2.3 联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
1 | let strNum: string | number; |
2.4 对象的类型——接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
例子
1
2
3
4
5
6
7
8
9
10// 接口 Person (接口一般首字母大写)
interface Person {
name: string;
age: number;
}
let man: Person = {
name: 'Tom',
age: 25
};
定义的变量比接口多/少了一些属性都是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致
(1) 可选属性
可选属性的含义是该属性可以不存在,但不允许添加未定义的属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20interface 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) 任意属性
1
2
3
4
5
6
7
8
9
10interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let man: Person = {
name: 'Tom',
gender: 'male'
};使用
[propName: string]定义了任意属性取string类型的值。一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
(3) 只读属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface 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 数组的类型
类型 + 方括号
数组的项中不允许出现其他的类型:
1
2
3let fibonacci: number[] = [1, '1', 2, 3, 5];
// Type 'string' is not assignable to type 'number'.数组泛型
1
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
用接口表示数组
1
2
3
4interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];类数组
类数组(Array-like Object)不是数组类型,比如
arguments:1
2
3
4
5
6
7function sum() {
let args: {
[index: number]: any;
length: number;
callee: Function;
} = arguments;
}any 在数组中的应用
1
let list: any[] = ['1', 1, { key: 'value' }];
2.6 函数的类型
函数声明
1
2
3
4
5
6
7
8function 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.函数表达式
1
2
3let mySum = function (x: number, y: number): number {
return x + y;
};上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的
mySum,是通过赋值操作进行类型推论而推断出来的。
如果需要我们手动给 mySum 添加类型,则应该是这样:
1 | let mySum: (x: number, y: number) => number = function (x: number, y: number): number { |
注意不要混淆了 TypeScript 中的
=>和 ES6 中的=>。在 TypeScript 的类型定义中,
=>用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用接口定义函数的形状
1
2
3
4
5
6
7
8interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用
?表示可选的参数:1
2
3
4
5
6
7
8
9function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
1
2
3
4
5
6
7
8
9
10
11function 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 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制
1
2
3
4
5function 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 仅用来定义类型
- 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
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
1 | let strNumberList: [string, number]; |
3.2 枚举
enum类型是对JavaScript标准数据类型的一个补充。
1 | enum Color {Red, Green, Blue} // 默认从0开始编号 |
3.3 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
二、TS 在 Vue2.x 的实践
1. 构建
通过官方脚手架构建安装 (Vue CLI 3)
1 | // 1. 如果没有安装 Vue CLI 就先安装 |
然后,命令行会要求选择预设。使用箭头键选择 Manually select features。
接下来,只需确保选择了 TypeScript 和 Babel 选项,然后配置其余设置,设置完成 vue cli 就会开始安装依赖并设置项目 。
2. 目录解析
安装完成打开项目,目录结构如下:
1 | |-- project-name |
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 推荐配置
1 | // tsconfig.json |
注意: 需要引入 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 语法之后更加扁平化
1 | <script> |
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) 组件声明
- 引入组件:和原生写法一致,都需要先引入再注册
1 | <script lang="ts"> |
(2) 响应式data 和 Prop声明
data
类语法中可以直接定义为类的实例属性作为组件的响应式数据
prop
类语法实现组件 props 定义是通过装饰器
@Prop实现
1 | <script lang="ts"> |
- !: 表示一定存在,?: 表示可能不存在。这两种在语法上叫赋值断言
- @Prop(options: (PropOptions | Constructor[] | Constructor) = {})
- PropOptions,可以使用以下选项:type,default,required,validator
- Constructor[],指定 prop 的可选类型
- Constructor,例如 String,Number,Boolean 等,指定 prop 的类型
(3) 生命周期函数
生命周期钩子的使用和原先使用的区别:在类语法中直接将生命周期生命为方法(方法名称和生命周期名称一致)。
1 | public created(): void { |
(4) Watch 监听属性
类语法实现响应式的数据监听,是由 vue-property-decorator 依赖提供 @Watch 装饰器来完成。
@Watch(path: string, options: WatchOptions = {})
其中,options 包含两个属性
- immediate?:boolean 侦听开始之后是否立即调用该回调函数
- deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数
1 | import { Vue, Component } from 'vue-property-decorator'; |
(5) computed 计算属性
类语法中的计算属性的实现,是通过 get 取值函数。
1 | public get getName() { |
getName 是计算后的值,name 是被监听的值
(6) method
在类语法实现原生 vue 的方法的方式,即通过直接定义类方法成员。
1 | public clickFunc(): void { |
(7) 事件触发
ts 环境下 vue 的事件触发方式和 js 环境下是一致的,区别只是事件回调定义的地方不同(ts 定义为类的实例方法,js 定义在 methods 属性中)。
(8) ref
类语法中使用 ref 需要借助vue-property-decorator提供的@Ref装饰器
(9) mixins
类语法使用 mixins 需要继承vue-property-decorator提供的 Mixins 函数所生成的类。
Mixins 函数的参数是 Vue 实例类
(10) slots 和 scopedSlots
slots 和 scopedSlots 的使用方式和原生 vue 保持一致。
三、tsconfig.json
概述
如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是TypeScript项目的根目录。
tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:
使用tsconfig.json
- 不带任何输入文件的情况下调用
tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。 - 不带任何输入文件的情况下调用
tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
当命令行上指定了输入文件时,tsconfig.json文件会被忽略。
示例
1 | { |
"compilerOptions"可以被忽略,这时编译器会使用默认值。在这里查看完整的编译器选项列表。
"files"指定一个包含相对或绝对文件路径的列表。
"include"和"exclude"属性指定一个文件glob匹配模式列表。 支持的glob通配符有:
*匹配0或多个字符(不包括目录分隔符)?匹配一个任意字符(不包括目录分隔符)**/递归匹配任意子目录