个人博客

学习加油站


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

TS 入门和Vue实践

发表于 2021-04-10

一、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
2
3
4
5
6
7
let bool: boolean = false;
let num: number = 10;
let str: string = 'sip';

// var bool = false;
// var num = 10;
// var str = 'sip';
(2) 特殊类型
  • Any

    任意值(Any)用来表示允许赋值为任意类型。 在任意值上访问任何属性 / 调用任何方法都是允许的

    1
    2
    let notSure: any = 'any';
    notSure = 1;

    未声明类型的变量

    变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型: 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

    1
    2
    let something;
    // 等同于 let something: any;
  • Void

    void 类型像是与 any 类型相反,它表示没有任何类型 ;在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

    1
    2
    3
    function alertName(): void {
    alert('My name is Tom');
    }

    声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null:

    1
    let unusable: void = undefined;
  • Null 和 Undefined

    1
    2
    let 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
    4
    let u: void;
    let num: number = u;

    // Type 'void' is not assignable to type 'number'.
2.2 类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

以下代码虽然没有指定类型,但是会在编译的时候报错:

1
2
3
4
let num = 'seven';
num = 7;

// error TS2322: Type '7' is not assignable to type 'string'.

事实上,它等价于:

1
2
3
4
let num: string = 'seven';
num = 7;

// error TS2322: Type '7' is not assignable to type 'string'.

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

1
2
3
let num;
num = 'seven';
num = 7;
2.3 联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

1
2
3
4
5
6
7
8
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)来定义对象的类型。

  • 例子

    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
    20
    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) 任意属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
    }

    let man: Person = {
    name: 'Tom',
    gender: 'male'
    };

    使用 [propName: string] 定义了任意属性取 string 类型的值。

    1. 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

    2. 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

  • (3) 只读属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 数组的类型
  • 类型 + 方括号

    数组的项中不允许出现其他的类型:

    1
    2
    3
    let 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
    4
    interface NumberArray {
    [index: number]: number;
    }
    let fibonacci: NumberArray = [1, 1, 2, 3, 5];
  • 类数组

    类数组(Array-like Object)不是数组类型,比如 arguments:

    1
    2
    3
    4
    5
    6
    7
    function 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
    8
    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.
  • 函数表达式

    1
    2
    3
    let mySum = function (x: number, y: number): number {
    return x + y;
    };

    上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。

如果需要我们手动给 mySum 添加类型,则应该是这样:

1
2
3
  let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

  • 用接口定义函数的形状

    1
    2
    3
    4
    5
    6
    7
    8
    interface 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
    9
    function 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
    11
    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 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制

    1
    2
    3
    4
    5
    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 仅用来定义类型

  • 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
2
3
4
let strNumberList: [string, number];

strNumberList = ['hello', 10]; // OK
strNumberList = [10, 'hello']; // webstorm会报红, 编译会出错
3.2 枚举

enum类型是对JavaScript标准数据类型的一个补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
2
3
4
5
6
7
// 1. 如果没有安装 Vue CLI 就先安装

npm install --global @vue/cli

// 2. 创建一个新工程,并选择 "Manually select features (手动选择特性)" 选项

vue create project-name

然后,命令行会要求选择预设。使用箭头键选择 Manually select features。

接下来,只需确保选择了 TypeScript 和 Babel 选项,然后配置其余设置,设置完成 vue cli 就会开始安装依赖并设置项目 。

2. 目录解析

安装完成打开项目,目录结构如下:

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
|-- 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 推荐配置
1
2
3
4
5
6
7
8
9
10
11
12
// 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 语法之后更加扁平化

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
32
<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
props: {
propMessage: String
}
})
export default class App extends Vue {
// initial data
msg = 123

// use prop values for initial data
helloMsg = 'Hello, ' + this.propMessage

// lifecycle hook
mounted () {
this.greet()
}

// computed
get computedMsg () {
return 'computed ' + this.msg
}

// method
greet () {
alert('greeting: ' + this.msg)
}
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

@Component({
components: {
HelloWorld,
},
})
export default class App extends Vue {

}
</script>
(2) 响应式data 和 Prop声明
  • data

    类语法中可以直接定义为类的实例属性作为组件的响应式数据

  • prop

    类语法实现组件 props 定义是通过装饰器@Prop实现

1
2
3
4
5
6
7
8
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
private name: string;
@Prop() private msg!: string;
}
  • !: 表示一定存在,?: 表示可能不存在。这两种在语法上叫赋值断言
  • @Prop(options: (PropOptions | Constructor[] | Constructor) = {})
    • PropOptions,可以使用以下选项:type,default,required,validator
    • Constructor[],指定 prop 的可选类型
    • Constructor,例如 String,Number,Boolean 等,指定 prop 的类型
(3) 生命周期函数

生命周期钩子的使用和原先使用的区别:在类语法中直接将生命周期生命为方法(方法名称和生命周期名称一致)。

1
2
3
4
5
6
public created(): void {
console.log('created');
}
public mounted(): void {
console.log('mounted')
}
(4) Watch 监听属性

类语法实现响应式的数据监听,是由 vue-property-decorator 依赖提供 @Watch 装饰器来完成。

  • @Watch(path: string, options: WatchOptions = {})

  • 其中,options 包含两个属性

    • immediate?:boolean 侦听开始之后是否立即调用该回调函数
    • deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数
1
2
3
4
5
6
7
8
9
10
11
12
import { Vue, Component } from 'vue-property-decorator';

@Component
export default class Index extends Vue {

//watch定义,其中Wacth装饰器第一个参数:响应式数据字符串(也可以定义为'a.b');
//第二个参数options成员[immediate,deep]分别对应的是原生的用法
@Watch('$route', { immediate: true })
private changeRouter(val: Route, oldVal: Route) {
console.log('$route watcher: ', val, oldVal);
}
}
(5) computed 计算属性

类语法中的计算属性的实现,是通过 get 取值函数。

1
2
3
public get getName() {
return 'computed ' + this.name;
}

getName 是计算后的值,name 是被监听的值

(6) method

在类语法实现原生 vue 的方法的方式,即通过直接定义类方法成员。

1
2
3
4
public clickFunc(): void {
console.log(this.name)
console.log(this.msg)
}
(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
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
// 编译选项
"compilerOptions": {
// 编译输出目标 ES 版本
"target": "esnext",
// 采用的模块系统
"module": "es2015",
// 以严格模式解析
"strict": true,
"jsx": "preserve",
// 如何处理模块
"moduleResolution": "node",
// 启用装饰器
"experimentalDecorators": true,
// 允许从没有设置默认导出的模块中默认导入
"allowSyntheticDefaultImports": true,
// 定义一个变量就必须给它一个初始值
"strictPropertyInitialization" : false,
// 允许编译javascript文件
"allowJs": true,
// 是否包含可以用于 debug 的 sourceMap
"sourceMap": true,
// 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
"noImplicitThis": false,
// 解析非相对模块名的基准目录
"baseUrl": ".",
// 给错误和消息设置样式,使用颜色和上下文。
"pretty": true,
// 设置引入的定义文件
"types": ["webpack-env", "mocha", "chai"],
// 指定特殊模块的路径
"paths": {
"@/*": ["src/*"]
},
// 编译过程中需要引入的库文件的列表
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
// ts 管理的文件
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
// ts 排除的文件
"exclude": ["node_modules"]
}

"compilerOptions"可以被忽略,这时编译器会使用默认值。在这里查看完整的编译器选项列表。

"files"指定一个包含相对或绝对文件路径的列表。

"include"和"exclude"属性指定一个文件glob匹配模式列表。 支持的glob通配符有:

  • * 匹配0或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录
# TypeScript
z-index和层叠上下文
  • 文章目录
  • 站点概览

WYP

知识管理,自我管理
12 日志
8 标签
GitHub E-Mail 简书
  1. 1. 一、TS 快速上手
    1. 1.1. 1. 关于TS
    2. 1.2. 2. 基础
      1. 1.2.1. 2.1 基本语法
        1. 1.2.1.1. (1) 原始值类型
        2. 1.2.1.2. (2) 特殊类型
      2. 1.2.2. 2.2 类型推论
      3. 1.2.3. 2.3 联合类型
      4. 1.2.4. 2.4 对象的类型——接口
      5. 1.2.5. 2.5 数组的类型
      6. 1.2.6. 2.6 函数的类型
      7. 1.2.7. 2.7 声明文件
    3. 1.3. 3. 补充
      1. 1.3.1. 3.1 元组 Tuple
      2. 1.3.2. 3.2 枚举
      3. 1.3.3. 3.3 泛型
  2. 2. 二、TS 在 Vue2.x 的实践
    1. 2.1. 1. 构建
      1. 2.1.0.1. 通过官方脚手架构建安装 (Vue CLI 3)
  3. 2.2. 2. 目录解析
    1. 2.2.0.1. tsconfig.json 推荐配置
  • 2.3. 3. 使用
    1. 2.3.1. 3.1 在 vue 中使用 typescript 常用的几个库
    2. 2.3.2. 3.2 上手
    3. 2.3.3. 3.2.1 vue-class-component
    4. 2.3.4. 3.2.2 vue-property-decorator
      1. 2.3.4.1. (1) 组件声明
      2. 2.3.4.2. (2) 响应式data 和 Prop声明
      3. 2.3.4.3. (3) 生命周期函数
      4. 2.3.4.4. (4) Watch 监听属性
      5. 2.3.4.5. (5) computed 计算属性
      6. 2.3.4.6. (6) method
      7. 2.3.4.7. (7) 事件触发
      8. 2.3.4.8. (8) ref
      9. 2.3.4.9. (9) mixins
      10. 2.3.4.10. (10) slots 和 scopedSlots
  • 3. 三、tsconfig.json
    1. 3.0.1. 概述
    2. 3.0.2. 使用tsconfig.json
    3. 3.0.3. 示例
  • © 2019 – 2021 wyp