一 重塑“类型思维”
- JavaScript是一门动态弱类型语言。
1 什么是TypeScript?
- 根据官方定义,它是拥有类型系统的JavaScript的超集,可以编译成纯JavaScript。
- 需要注意三个要点:
(1)类型检查 - TS会在编译代码时进行严格的静态类型检查,这意味着可以在编码阶段发现可能存在的隐患,而不是带到线上去。
(2)语言扩展 - TS会包括来自ES6和未来提案中的特性,如异步操作和装饰器,也会从其他语言借助某些特性,如接口和抽象类。
(3)工具属性 - TS可以编译成标准的JS,可以在任何浏览器和操作系统上运行,从这个角度讲,它更像是个工具,而不是一门独立的语言。
2 为什么要使用TypeScript?
其他好处,比如:
- VSCode具有强大的自动补全、导航和重构功能,这使得接口定义可以直接代替文档,同时也可以提高开发效率,降低维护成本。
- TS可以帮团队重塑“类型思维”,接口的提供方将被迫去思考API的边界,他们将从代码的编写者蜕变为代码的设计者。
二 类型基础
动态类型与静态类型
1 概念
-
静态类型语言:在编译阶段确定所有变量的类型,如C++语言:
class C { public: int x int y } int add(C a, C b) { return a.x + a.y + b.x + b.y }
-
动态类型语言:在执行阶段确定所有变量的类型,如JavaScript语言:
class C { constructor(x, y) { this.x = x this.y = y } } function add(a, b) { return a.x + a.y + b.x + b.y }
2 从内存分配的角度对比JS与C++
3 总结
静态类型语言 | 动态类型语言 |
---|---|
对类型极度严格 | 对类型非常宽松 |
立即发现错误 | bug可能隐藏数月甚至数年 |
运行时性能好 | 运行时性能差 |
自文档化 | 可读性差 |
强类型语言和弱类型语言
- 强类型语言:不允许程序在发生错误后继续执行
语言类型象限
三、编写Hello World ts程序
1 准备工作
预装软件NodeJs和VSCode
新建文件夹ts_in_action
npm命令初始化工程:
$ npm init -y
,生成package.json文件- 全局安装TypeScript:
$ npm i typescript -g
,这样我们可以在任何地方使用ts的编译器tsc,查看编译器的帮助信息$ tsc -h
- 创建配置项:
$ tsc --init
,生成tsconfig.json文件 - 新建src/index.ts文件:
let hello: string = 'hello typescript'
,对其进行编译:$ tsc ./src/index.ts
,会生成一个src/index.js文件:var hello = 'hello typescript'
2 配置构建工具,使用webpack
2.1 安装:$ npm i webpack webpack-cli webpack-dev-server -D
,此属于开发环境配置
2.2 创建build目录,用来存放所有的配置文件:
(1)webpack.base.config.js - 是公共环境的配置
-
指定入口文件:
entry: './src/index.ts'
-
配置输出文件:
output: { filename: 'app.js' }
-
输入的目录使用默认的dist目录,指定三个扩展名:
resolve: { extensions: ['.js', '.ts', '.tsx'] }
-
安装ts相应的loader,并再次本地安装ts:
$ npm i ts-loader typescript -D
module: { rules: [ { test: /\.tsx?$/i, use: [{ loader: 'ts-loader' }], exclude: /node_modules/ } ]
} -
安装使用插件html-webpack-plugin:
$ npm i html-webpack-plugin -D
,通过一个模板帮助我们生成一个网站的首页,并把输出文件自动嵌入到这个html文件中:const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { ......, plugins: [ new HtmlWebpackPlugin({ template: './src/tpl/index.html' }) ] }
-
新建并编写这个模板文件src/tpl/index.html(输入html:5),修改title,在body中插入div:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>TypeScript</title> </head> <body> <div class="app"></div> </body> </html>
(2)webpack.dev.config.js - 是开发环境的配置
-
使用sourceMap,采用官方推荐的:
module.exports = { devtools: 'cheap-module-eval-source-map' }
PS:- cheap表示sourceMap会忽略文件的列信息;
- module会定位到ts源码,而不是经过loader转义后的js源码;
- eval-source-map指会将sourceMap以dataUrl的形式打包到文件中,它的重编译速度很快,所以不必担心性能问题。
(3)webpack.pro.config.js - 是生产环境的配置
-
安装插件
$ npm i clean-webpack-plugin -D
,它的作用是每次成功构建时,清空dist目录:const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { plugins: [ new CleanWebpackPlugin() ] }
(4)webpack.config.js - 是所有配置文件的入口
-
安装插件
$ npm i webpack-merge -D
,它的作用是把两个配置文件合并:const merge = require('webpack-merge') const baseConfig = require('./webpack.base.config') const devConfig = require('./webpack.dev.config') const proConfig = require('./webpack.pro.config') let config = process.NODE_ENV === 'development' ? devConfig : proConfig module.exports = merge(baseConfig, config)
3 修改npm脚本
3.1 package.json文件
-
更改入口:
"main": "./src/index.ts",
-
编写启动开发环境的命令并运行
$ npm start
,其中mode参数的作用是设置当前的环境变量,config是用来指定配置文件的:"scripts": { "start": "webpack-dev-server --mode=development --config ./build/webpack.config.js", },
-
编写生产环境的命令并运行
$ npm run build
,可以看到生成一个dist目录:"build": "webpack --mode=production --config ./build/webpack.config.js",
4 TS的基本类型
ES6的数据类型
- 6种基本数据类型:Boolean、Number、String、Symbol、undefined、null
- 3种引用类型:Array、Function、Object
TS的数据类型
- 在ES6的基础上新增了多种类型:void、any、never、元组、枚举、高级类型
4.1 类型注解
- 作用:相当于强类型语言中的类型声明
- 语法:(变量/函数): type
4.2 新建src/datatype.ts,里面定义各种类型的数据:
-
原始类型:
let bool: boolean = true let num: number = 123 let str: string = 'abc'
-
数组:
let arr1: number[] = [1, 2, 3] let arr2: Array<number> = [1, 2, 3]
PS:
- 以上的含义是该数组只能是number;
- 但如果我们需要添加其他类型,就要采用联合类型方式:
let arr2: Array<number | string> = [1, 2, 3, '4']
-
元组 - 是特殊的数组,它限定了数组元素的类型和个数:
let tuple: [number, string] = [0, '1']
PS:我们可以通过push方法为元组添加新元素,但是不能越界访问,因此实际工作中不建议这样使用:
tuple.push(2)
console.log(tuple) // [0, '1', 2]
tuple[2] // 报错 -
函数:
let add = (x: number, y: number): number => x + y
PS:
以上包括参数和返回值类型注解,通常返回值类型注解可以省略,这就利用了ts的类型推断功能。
也可以这样定义:
let compute: (x: number, y: number) => number
compute = (a, b) => a + b -
对象:
let obj: object = {x: 1, y: 2} obj.x // 提示错误,因为不确定里面是否包含x属性 // 改成这样就可以了: let obj: {x: number, y: number} = {x: 1, y: 2} obj.x
-
symbol - 含义是具有唯一的值:
let s1: symbol = Symbol() let s2 = Symbol() console.log(s1 === s2) // false
-
undefined/null:
let un: undefined = undefined let nu: null = null num = undefined // 提示错误 num = null // 提示错误
PS:
把一个变量赋值成undefined/null时,只能取undefined/null值,不能取其他类型的值;反之,其他类型却可以赋值给undefined/null,只不过需要对tsconfig.json做如下设置:
"strictNullChecks": false,
PS:
但是这种方式一般不推荐,如果非要对其他类型赋值undefined/null,最好使用联合类型,上面就不会报错了:
let num: number | undefined | null = 123 -
void是js中的一种操作符,它可以让任何表达式返回undefined,如:
void 0 // undefined
PS:
在ts中,void类型表示没有任何返回值的类型,比如一个没有任何返回值的函数:
let noReturn = () => {} -
any类型 - 不定义类型时默认为any类型,可以任意赋值,不建议使用
-
never表示永远不会有返回值的类型,有两种情况:抛出异常函数和死循环函数
let error = () => { throw new Error('error') } let endless = () => { while(true) {} }
5 TS的枚举类型
5.1 概念
- 枚举是指一组有名字的常量集合,可以理解成手机里的通讯录,我们只需要记住人名即可,不需要记号码
5.2 分类
-
数字枚举 - 枚举值会递增,并采用反向映射的原理
enum Role { Reporter = 2, Developer, Maintainer, Owner, Guest } console.log(Role.Reporter) // 2 console.log(Role[4]) // Maintainer console.log(Role) // => // [2: "Reporter" // 3: "Developer" // 4: "Maintainer" // 5: "Owner" // 6: "Guest" // Developer: 3 // Guest: 6 // Maintainer: 4 // Owner: 5 // Reporter: 2]
-
字符串枚举 - 编译结果显示只有成员名称作为key,所以它是不能进行反向映射的
enum Message { Success = '成功', Fail = '失败' }
-
异构枚举 - 数字枚举和字符串枚举混用的结果,容易引起混淆,不建议使用
enum Answer { N, Y = 'yes' }
5.3 枚举成员
-
性质 - 枚举成员的值是只读类型,定义后不能修改
-
分类
(1)const member(常量枚举),包括三种情况:没有初始值的情况、对已有枚举成员的引用、常量的表达式,常量枚举成员会在编译的时候计算结果,然后以常量的形式出现在运行时环境。enum Char { a, b = Char.a, c = 1 + 4 }
(2)computed member,指需要被计算的枚举成员,是一些非常量的表达式,这些枚举成员的值不会在编译阶段计算,而是在程序执行阶段计算。
enum Char { d = Math.random(), e = '123'.length, f // 提示错误 }
PS:
特别注意:在computed member后的成员一定要赋给初始值,否则会提示错误
5.4 常量枚举
-
特性:会在编译阶段被移除
-
作用:当我们不需要一个对象,而需要这个对象的值时,就需要使用常量枚举,这样能减少在编译环境的代码,比如:
const enum Month { Jan, Feb, Mar } let month = [Month.Jan, Month.Feb, Month.Mar]
PS: 会被编译成: var month = [0 /* Jan */, 1 /* Feb */, 2 /* Mar */];
5.5 枚举类型
- 在某些情况下,枚举和枚举成员都可以作为一种单独类型存在。
第一种情况,枚举成员没有任何初始值;
第二种情况,所有成员都是数字枚举;
第三种情况,所有成员都是字符串枚举。
enum E { a, b } enum F { a = 0, b = 1 } enum G { a = 'apple', b = 'banana' }
-
对于第一种和第二种情况,可以把任意的number类型赋值给枚举类型,它的取值也可以超出枚举成员的定义,但是两种不同类型的枚举不能进行比较:
let e: E = 3 let f: F = 3 // console.log(e === f) // 提示错误
let e1: E.a = 1 let e2: E.b // console.log(e1 === e2) // 提示错误 let e3: E.a = 1 console.log(e1 === e3) // true
-
而第三种情况字符串枚举的取值只能是枚举成员的类型:
let g1: G = G.a let g2: G.a = G.a
6 接口(1):对象类型接口
6.1 定义接口
interface List { id: number; name: string; } interface Result { data: List[] } function render(result: Result) { result.data.forEach((value) => { console.log(value.id, value.name) }) } let result = { data: [ {id: 1, name: 'A'}, {id: 2, name: 'B'} ] } render(result) // 1 "A" // 2 "B"
PS:
后端有时候会传来约定之外的字段,ts并不报错。所以只要传入的对象是必要条件就是被允许的:
let result = {
data: [
{id: 1, name: 'A', sex: 'male'},
{id: 2, name: 'B'}
]
}
但是我们直接传入对象字面量,ts就会对额外的字段进行类型检查:
render({
data: [
{id: 1, name: 'A', sex: 'male'}, // 提示错误
{id: 2, name: 'B'}
]
})
6.2 绕过类型检查的方法
-
第一种方式,是像我们上面那样,将对象赋值给一个变量
-
第二种方式是类型断言:as + 对象的类型,我们明确地告诉编译器,对象的类型就是Result,编译器就会绕过类型检查
render({ data: [ {id: 1, name: 'A', sex: 'male'}, // 提示错误 {id: 2, name: 'B'} ] } as Result)
PS:
类型断言的另一种不建议用的方法,就是在对象前面加上<Result>,但是在React种容易产生歧义。 -
第三种方法是使用字符串索引签名,格式如下:
interface List { id: number; name: string; [x: string]: any; } PS: 该签名的含义是用任意的字符串去索引List,会得到任意的结果,这样List就支持多个属性了。
6.3 可选属性(属性+? 格式)
-
假设有个新需求,需要判断value中是否有个新字段,如果有,就把它打印出来:
interface List { id: number; name: string; age?: number; } interface Result { data: List[] } function render(result: Result) { result.data.forEach((value) => { console.log(value.id, value.name) if(value.age) { console.log(value.age) } }) } let result = { data: [ {id: 1, name: 'A', sex: 'male'}, {id: 2, name: 'B'} ] } render(result) PS: 在render函数中进行判断,会提示错误,这时我们在List中添加属性age,result会报错,这就需要我们使用可选属性了。
6.4 只读属性(readonly + 属性 格式)
-
只读属性不允许修改:
interface List { readonly id: number; } function render(result: Result) { result.data.forEach((value) => { value.id ++ // 提示错误 }) }
6.5 可索引类型的接口
以上属性的个数是固定的,当我们不确定属性个数时,就要用到可索引类型的接口,常用的有两种:
-
用数字索引的接口:
interface StringArray { [index: number]: string } PS: 含义是用任意的数字去索引StringArray,会得到一个string,这就相当于声明了一个字符串类型的数组。
比如:let chars: StringArray = ['A', 'B']
-
用字符串索引的接口:
interface Names { [x: string]: string } PS: 含义是用任意的字符串索引Names,得到的结果都是string,这样我们就不能并列声明number类型的成员了:
interface Names { [x: string]: string y: number // 提示错误 z: string }
-
这两种索引签名是可以混用的:
interface Names { [x: string]: string [z: number]: string } PS: 需要注意的是,数字索引签名的返回值一定要是字符串索引签名返回值的子类型,这是因为js会进行类型转换,将number转成string,这样就能保证类型的兼容性。
比如下面这样就会报错:interface Names { [x: string]: string [z: number]: number // 提示错误 }
但这样就可以:
interface Names { [x: string]: any [z: number]: number }
7 接口(2):函数类型接口
7.1 定义接口
-
用变量定义函数类型:
let add: (x: number, y: number) => number
-
用接口定义函数类型:
interface Add { (x: number, y: number): number }
-
用类型别名定义函数类型(更简洁):
type Add = (x: number, y: number) => number let add: Add = (a, b) => a + b
7.2 混合类型的接口
-
这种接口既可以定义一个函数,也可以像对象一样拥有属性和方法:
interface Lib { (): void version: string doSomething(): void } let lib: Lib = (() => {}) as Lib lib.version = '1.0' lib.doSomething = () => {}
-
改造一下上例,我们可以创造多个lib实例:
function getLib() { let lib: Lib = (() => {}) as Lib lib.version = '1.0' lib.doSomething = () => {} return lib } let lib1 = getLib() lib1() lib1.doSomething() let lib2 = getLib()
8 函数相关知识点梳理
8.1 定义函数的四种方式
-
第一种,用function定义,需要明确地指出参数的类型,而函数的返回值可以通过ts的类型推断省去:
function add1(x: number, y: number) { return x + y }
-
第二种,通过一个变量定义函数类型
let add2: (x: number, y: number) => number
-
第三种,通过类型别名定义函数类型
type add3 = (x: number, y: number) => number
-
第四种,通过接口定义函数类型
interface add4 { (x: number, y: number): number } PS: 注意:后三种只是定义函数类型,而没有具体的实现。
8.2 函数参数
-
在ts中形参和实参必须一一对应,多一个少一个都不行。
-
可选参数(格式:参数名 + ?),即可传可不传,需要注意的是可选参数必须位于必选参数之后:
function add5(x: number, y?: number) { return y ? x + y : x } console.log(add5(2)) // 2
-
为参数提供默认值:
function add6(x: number, y = 1, z: number, q = 4) { return x + y + z + q } console.log(add6(1, undefined, 3)) // 9 PS: 需要注意的是,在必选参数前,默认参数不可省略,必须明确传入undefined来获取默认值。
-
以上参数的个数都是固定的,当参数不确定时,就可以使用剩余参数(格式:...参数集合: 类型):
function add7(x: number, ...rest: number[]) { return x + rest.reduce((pre, cur) => pre + cur) } console.log(add7(2, 3, 2, 13, 4)) // 24
8.3 函数重载
-
其他语言的函数重载:
含义:如果两个函数名称相同,但是参数类型和个数不同,就实现了函数重载。
好处:不需要为了相似功能的函数选用不同的函数名称,这样增强了函数的可读性。 -
ts的函数重载要求我们先定义一系列名称相同的函数声明,然后再定义一个更宽泛的函数:
function add8(...rest: number[]): number function add8(...rest: string[]): string function add8(...rest: any[]): any { let first = rest[0] if(typeof first === 'string') { return rest.join('-') } if(typeof first === 'number') { return rest.reduce((pre, cur) => pre + cur) } } console.log(add8(1, 2, 3)) // 6 console.log(add8('a', 'b', 'c')) // a-b-c PS: ts编译器在处理重载时,会去查询一个重载的列表,也就是我们前面定义的这个列表,并且会尝试第一个定义,如果匹配,就使用这个函数定义,如果不匹配,就接着往下查找,所以我们要把最容易匹配的函数定义写到最前面。
9 类(1):继承和成员修饰符
9.1 类的基本实现
-
定义一个Dog类,与ES不同的是,我们为成员属性添加了类型注解,也为构造函数的参数增加了类型注解。
class Dog { constructor(name: string) { this.name = name } name: string run() {} } 需要注意的是: (1)无论是在ES还是ts中,类成员的属性都是实例属性,而不是原型属性;类成员的方法也都是实例方法。对比可见:
console.log(Dog.prototype) // {run: ƒ, constructor: ƒ} let dog = new Dog('wangwang') console.log(dog) // Dog {name: "wangwang"}
(2)与ES不同的是,实例的属性必须具有初始值,或者在构造函数中被初始化。 以下√的三种方式均是可以的:
class Dog { constructor(name: string) { // this.name = name √ } // name: string = 'dog' √ // name?: string = 'dog' √ run() {} }
9.2 类的继承(用extends关键字)
class Husky extends Dog { constructor(name: string, color: string) { super(name) this.color = color } color: string } 需要注意的是:
(1)派生类的构造函数必须包含‘super’调用,而且参数必须包含父类的参数。
(2)新添加的属性要初始化,一般在constructor中,而且用到的this必须在super调用后才能调用。
9.3 类的成员修饰符(这是ts对ES的扩展)
-
公有成员:对所有人都是可见的,格式public+属性/函数名
-
私有成员,也能在类的本身被调用,而不能被类的实例调用,也不能被子类调用,格式private+属性/函数名
class Dog { constructor(name: string) { this.name = name } name: string run() {} private pri() {} } let dog = new Dog('wangwang') dog.pri() ❌ class Husky extends Dog { constructor(name: string) { super(name) this.pri() ❌ } } PS: 也可以给构造函数加上私有成员属性,作用是类既不能被实例化,也不能被继承,都会提示错误。
class Dog { private constructor(name: string) { this.name = name } name: string } let dog = new Dog('wangwang') ❌ class Husky extends Dog { } ❌
-
受保护成员,只能在类或子类中访问,而不能在类的实例中访问,格式protected+属性/函数名。
class Dog { constructor(name: string) { this.name = name } name: string protected pro() {} } let dog = new Dog('wangwang') dog.pro() ❌ class Husky extends Dog { constructor(name: string) { super(name) this.pro() ✔ } }
PS:
构造函数也能被声明为protected,作用是这个类不能被实例化,只能被继承,就相当于声明了一个基类。 class Dog { protected constructor(name: string) { this.name = name } name: string } let dog = new Dog('wangwang') // ❌ class Husky extends Dog { ...... } -
只读属性:不可以被更改,切记它跟实例属性一样,一定要被初始化。
class Dog { constructor(name: string) { this.name = name } name: string readonly legs: number = 4 }
-
构造函数的参数也可以添加修饰符,它的作用是将参数自动变成了实例的属性,这样就能省略在类中的定义了。
class Husky extends Dog { constructor(name: string, public color: string) { super(name) this.color = color } // color: string }
-
静态成员:只能通过类名来调用,而不能通过子类调用,它是可以被继承的,格式static修饰符+属性名。
class Dog { constructor(name: string) { this.name = name } name: string static food: string = 'bones' } let dog = new Dog('wangwang') console.log(Dog.food) // ✔ // console.log(dog.food) ❌ class Husky extends Dog { constructor(name: string, public color: string) { super(name) this.color = color } } console.log(Husky.food) // ✔
10 类(2):抽象类与多态
10.1 抽象类
-
es中并没有引用抽象类的概念,这是ts对es又一次扩展,所谓抽象类就是只能被继承而不能被实例化的类。举例说明:
// 定义抽象类Animal abstract class Animal { // 定义方法,子类可以复用 eat() { console.log('eat') } // 也可以不指定方法的具体实现,这就构成了抽象方法 abstract sleep(): void // 好处是,明确知道子类可以有其他的实现,就没必要在父类中实现了 } // 实例化 // let animal = new Animal() // ❌提示无法创建抽象类的实例,说明它只能被继承 class Dog extends Animal { constructor(name: string) { super() this.name = name } name: string run() {} sleep() { console.log('dog sleep') } } let dog = new Dog('wangwang') dog.eat() PS: 抽象类的好处是可以抽离出一些事物的共性,有利于代码的复用和扩展。
-
抽象类也可以实现多态,所谓多态就是在父类中定义一个抽象方法,在多个子类中对这个方法有不同的实现,在程序运行时会根据不同的对象执行不同的操作,这样就实现了运行时的绑定。
10.2 多态
-
紧接上例:
class Cat extends Animal { sleep() { console.log('Cat sleep') } } let cat = new Cat() let animals: Animal[] = [dog, cat] animals.forEach(i => { i.sleep() })
10.3 特殊的ts类型:this类型
-
类的成员方法可以直接返回一个this,可以很方便地实现链式调用。
class WorkFlow { step1() { return this } step2() { return this } } new WorkFlow().step1().step2()
-
在继承的时候,this类型也可以表现出多态,这里的多态是指this既可以是父类型也可以是子类型。
class MyFlow extends WorkFlow { next() { return this } } new MyFlow().next().step1().next().step2() PS: 以上调用先返回子类的类型再返回父类的类型然后再返回子类的类型,这样就保持了父类和子类之间接口调用的连贯性。
11 类与接口的关系
11.1 类类型接口