前言
在 【浅谈 TypeScript【上】】中,简单讲述了关于JavaScript静态类型检查工具Flow的用法等。可以看到,我们接下来讲述的TypeScript与它其实有很多相似之处。
TS 与 JS
TypeScript 并不是一个完全新的语言, 它是 JavaScript 的超集,为 JavaScript 的生态增加了类型机制,并最终将代码编译为纯粹的 JavaScript 代码。
TypeScript | JavaScript |
JavaScript 的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页。 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成 JavaScript 代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
支持 ES3,ES4,ES5 和 ES6 等 | 不支持编译其他 ES3,ES4,ES5 或 ES6 功能 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
TypeScript 概述
首先,我们先来看一下 TypeScript的范围图:
TypeScript 是由微软开发的一种开源、跨平台的编程语言,是JavaScript 的超集,主要提供了类型系统和对 ES6 的支持,最终会被编译为JavaScript代码。可以说,TypeScript是前端领域中的第二语言,属于渐进式语言。
一、定义
引用官网定义:
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.
中文意思:
TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。
二、特点
- 自动转换新特性,最低可以编译到ES3版本;
- 任何一种 JavaScript 运行环境都支持;
- 相比于Flow,功能更为强大,生态也更健全、更完善。
三、TypeScript的优势和缺点
TypeScript的优势
- TypeScript 增加了代码的可读性和可维护性
a. 类型系统可以方便用户快速的使用类型定义过后的函数变量;
b. 在编译阶段发现错误,节省了运行之后出错的维护时间;
c. 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。 - TypeScript 非常包容
a. TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可;
b. 可以根据变量的值等进行类型推论,可以定义从简单到复杂的几乎一切类型;
c. TypeScript 编译报错,不会影响生成 JavaScript 文件;
d. 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取。 - TypeScript 拥有活跃的社区
a. 大部分第三方库都有提供给 TypeScript 的类型定义文件;
b. Google 开发的 Angular2 就是使用 TypeScript 编写的;
c. TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范。
TypeScript的缺点
- TypeScript 增加了接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师不熟悉的概念,需要一定的学习成本;
- 项目初期,TypeScript 会增加一些开发成本,需要去编写一些类型的定义。但在后期维护时,会减少维护成本。
- 集成到构建流程需要一些工作量;
- 可能和一些库结合的不是很完美。
快速上手
- 以下操作需在 cmd 命令行界面进行,并使用 yarn (或 npm/cnpm)执行安装运行命令。
- 初始化包管理文件 package.json
yarn init --yes // --yes 表示快速初始化
- 安装 TypeScript
yarn add typescript --dev // --dev 表示项目内安装,而非全局安装
- 新建 .ts 结尾的 hello.ts文件,使用 tsc 命令将 .ts文件编译为 .js文件
yarn tsc hello.ts // 进入到当前目录,否则需加上文件路径
- 解析
编译过程中,先检查类型是否使用异常,然后移除一些类型注解等扩展语法,并且会自动转换 ECMAScript 的新特性。
配置文件
- 在项目根目录进行操作, tsconfig.json 配置属性将简单介绍。
- 初始化 tsconfig.json 配置文件
yarn tsc --init
- tsconfig.json 配置属性
{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version */
"module": "commonjs", /* Specify module code generation */
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "dist", /* Redirect output structure to the directory. */
"rootDir": "src", /* Specify the root directory of input files. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
– rootDir:编译源代码,一般在 src 文件夹中存放
– outDir:输出文件,一般放置在 dist 文件夹中
- 根据配置文件,编译 src 中全部的 .ts 文件
yarn tsc
基础语法
原始数据类型
在JavaScript中,原始数据类型分别为:数值类型、字符串类型、布尔类型、Null、Undefined以及ES6中新增的Symbol。
字符串类型
使用 string 定义字符串类型:
const a: string = 'foobar'
编译结果:
"use strict"; // 配置文件中,配置了严格模式为 true
var a = 'foobar';
数值类型
使用 number 定义数值类型:
const num: number = 100
const na: number = NaN
const infi: number = Infinity
编译结果:
var num = 100;
var na = NaN;
var infi = Infinity;
布尔类型
使用 boolean 定义布尔值类型:
const t: boolean = true
const f: boolean = false
编译结果:
var t = true;
var f = false;
空值
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:
function voidTest(): void {
// 函数体
}
声明变量的数据类型为 void 时,非严格模式下,变量的值可以为 undefined 或 null。而严格模式下,变量的值只能为 undefined。
const u: void = undefined
const t: void = null // 严格模式下,语法报错
Null 与 Undefined
使用 null 和 undefined 来定义 null 和 undefined 这两个原始数据类型:
const f: null = null
const g: undefined = undefined
编译结果:
var f = null;
var g = undefined;
注意
void类型的变量不能赋值给 number 、string、boolean类型的变量。
let u: void;
let num1: number = u;
// Type 'void' is not assignable to type 'number'.
严格模式下,null、undefined类型的变量不能赋值给 number 、string、boolean类型的变量。
let num2: number = undefined;
// Type 'undefined' is not assignable to type 'number'.
let num3: number = null;
// Type 'null' is not assignable to type 'number'.
Symbol
Symbol 是ES2015中新增的数据类型。因此,我们需要做一些操作,使程序在编译过程中可以识别 ES2015中的新特性。
解决方案
- 方案一:
将 tsconfig.json 文件中的 “target”,设为 “es2015” 以上版本。 - 方案二:
tsconfig.json 文件中的 “target” 还是为 “es5"等原来的版本,但要添加标准库(标准库就是内置对象所对应的声明),即在 tsconfig.json 文件中的 “lib” 数组中添加 “ES2015”,但是若只添加这一选项,则会覆盖原来的标准库,因此需要把原来的标准库添加回来,如"DOM”(在标准库中,"DOM"标准库包含了 dom 和bom)
const h: symbol = Symbol()
Object 类型
- 不是特指对象类型,而是泛指所有的非原始的数据类型,即包括 对象、数组、函数 等。
const foo: object = function () {}
const bar: object = []
const baz: object = {}
// 类似对象字面量:属性类型数量要一一对应
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
数组类型
声明数组的两种形式:
- 使用 Array泛型
const arr1: Array<number> = [1, 2, 3]
- 使用 数据类型 + [] 形式,这种比较常用
const arr2: number[] = [1, 2, 3]
// 数据类型 + [], 常用的举例
function sum (...args: number[]) {
return args.reduce((prev, current) => prev + current, 0)
}
// sum(1, 2, 3, 'foo') // 报错
元组类型(Tuple Types)
元组就是一个明确元素数量以及元素类型的数组。各个元素的类型不必要完全相同。一般用于在一个函数中去返回多个返回值。
定义元组
- 使用类似 数组字面量 的形式,如果元素对应类型不相符,或者元素数量不一致,都会报错。
const tuple: [number, string] = [18, 'foo']
访问的两种形式
- 使用数组下标的形式
const age = tuple[0]
const name = tuple[1]
- 使用数组解构的方式,提取数组中的每个元素
const [age, name] = tuple
// 返回元组的例子
Object.entries({
foo: 123,
bar: 456
})
枚举类型
特点
1、给一组数值去分别取上一个更好理解的名字;
2、一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性。
语法和注意
- js语法:使用对象模拟实现枚举
const PostStatus = {
Draft: 0,
Unpublished: 1,
Published: 2
}
- TypeScript语法:使用 enum 关键字, 注意使用的是 “=”
const PostStatus = {
Draft = 0,
Unpublished = 1,
Published = 2
}
- 可以不指定具体的值,默认从 0 开始累加
enum PostStatus {
Draft,
Unpublished,
Published
}
- 也可以给第一个指定具体的值,后面的值将会在第一个指定值的基础上,执行累加
enum PostStatus {
Draft = 3,
Unpublished,
Published
}
- 给定的值,既可以是数值型,也可以是字符串,即字符串枚举
// 由于字符串无法累加,因此需要自行赋值。字符串枚举并不常见
enum PostStatus {
Draft = 'aaa',
Unpublished = 'bbb',
Published = 'ccc'
}
// PostStatus[0] // =>Draft 通过索引器的形式访问对应的枚举名称
枚举类型会影响编译的结果,最终会编译为双向的键值对对象。
编译结果:
// 目的:可以让我们动态的通过枚举值(0, 1, 2, ...)去获取枚举的名称
var PostStatus;
(function (PostStatus) {
PostStatus["Draft"] = "aaa";
PostStatus["Unpublished"] = "bbb";
PostStatus["Published"] = "ccc";
})(PostStatus || (PostStatus = {}));
- 如果确定不会使用索引器的形式访问枚举,那么建议使用常量枚举。
const enum PostStatus {
Draft,
Unpublished,
Published
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 2 // 1 // 0
}
函数类型
对函数的输入输出进行类型限制,输入:参数;输出:返回值。
定义函数的方式
- 方式一:函数声明
/**
* 如果某个参数可传可不传:
* 1、可在形参的参数名后面添加 "?" , 使其变成可选的;
* 2、使用 es6中添加参数默认值的方法, 使其变成可有可无的。
*
* 若需要接收任意个数的参数,使用 es6 中的rest操作符
*
*/
function func1 (a: number, b?: number, c: number = 10, ...rest: number[]): string {
return 'func1'
}
func1(100, 200)
注意:
1)形参列表都为必传参数时,传入的实参类型和数量,都必须与形参保持一致;
2)可选参数,必须放在参数列表的最后面。
- 方式二:函数表达式
这种对于回调函数的形参类型,需要进行约束。
const fun2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func2'
}
任意类型
any 就是用来接收任意类型数据的一种类型,属于动态类型,不会有任何的类型检查,很可能会存在类型安全的问题,轻易不要使用。
function stringify (value: any) {
return JSON.stringify(value)
}
stringify('string')
stringify(100)
stringify(true)
let foo: any = 'string'
foo = 100 // 在使用过程中,可以接收任意类型的数据
foo.bar() // 语法上不会报错
类型断言
类型断言是在编译过程中的概念,代码编译过后,将不会存在。而类型转换是代码运行时的概念。
// 假设这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]
const res = nums.find(i => i > 0)
// TypeScript 推断出 res: number | undefined,不能执行下面类似操作
const square = res * res // 报错,不可以执行
类型断言的两种方式
- 使用 as 关键字,明确告诉 TypeScript 这个数据的具体类型
const num1 = res as number
- 使用 <数据类型> 的方式,但 JSX下会有语法冲突,不能使用
const num2 = <number>res
接口(Interfaces)
什么是接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
作用
接口就是用来约束对象的结构。一个对象要是去实现一个接口,那么这个对象必须拥有接口中的所有成员。
语法
interface 接口名称 {
属性名1: 数据类型;
属性名2: 数据类型
...
属性名n: 数据类型
}
代码示例如下:
// 约定一个对象当中,具体应该有哪些成员,以及成员的类型又是什么样的
interface Post {
title: string; // 内部成员可用 ";" 分割,也可以不加
content: string
subtitle?: string // ? 可选成员
readonly summary: string // 只读成员
}
function pointPost (post: Post) {
console.log(post.title);
console.log(post.content);
}
const post: Post = {
title: 'Hello TypeScript',
content: 'A javascript superset',
summary: 'A javascript'
}
// post.summary = 'other' // 不可修改
pointPost(post)
// 添加动态成员
interface Cache {
[key: string]: string // key 代表属性名,可随意取名
}
const cache: Cache = {}
cache.foo = 'value1'
cache.bar = 'value2'
总结
TypeScript 中的接口只是用来为有结构的数据,做类型约束。在实际运行中,没有实际意义。
类(class)
概述
描述一类具体事物的抽象特征,主要用来描述一类具体对象的抽象成员。
声明属性
在TypeScript中要明确,在类型当中声明它所拥有的一些属性,而不是在构造函数当中动态通过this去添加。
/**
* ES2016新增,在类型当中声明属性的方式,就是直接在类中定义
*
* 注意:在TypeScript中,类的属性必须有一个初始值
*
* 属性赋初始值的方式:
* 1、在 "=" 后面赋值
* 2、在构造函数中进行初始化,动态的为属性赋值
*/
class Person {
name: string // = 'init name'
age: number
constructor (name: string, age: number) {
this.name = name
this.age = age
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}
类的访问修饰符
- 作用:
主要用来控制类当中成员的可访问级别。 - 分类
1)private :私有成员,只能在类的内部进行使用,外部访问不到,不允许继承。
2)public : 公有成员,默认就是 public,建议手动添加上,便于理解。
3)protected : 受保护的,外部访问不到,只允许在子类当中访问对应的成员,允许继承。
class Person {
public name: string // 公有属性
private age: number // 私有属性
protected gender: boolean // 受保护的,只能在子类中被访问
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
console.log(this.age)
}
}
const tom = new Person('tom', 18)
console.log(tom.name)
// console.log(tom.age) // 私有属性,外部访问不到,报错
// console.log(tom.gender) // 受保护的属性,只能在子类中访问,报错
构造函数被私有化时,则会发生以下问题:
1)构造函数被私有化,将不能在外部使用 new 关键字进行实例化;
2)需要在类中定义静态方法,并返回 创建的类的实例。
class Student extends Person {
private constructor (name: string, age: number) {
super(name, age)
console.log(this.gender);
}
static create (name: string, age: number) {
return new Student(name, age)
}
}
const jack = Student.create('jack', 20)
类的只读属性
class Person {
protected readonly gender: boolean // readonly 要放在访问修饰符的后面
constructor () {
this.gender = true // 注意:初始化过后,不可以再修改
}
}
类 与 接口
// 一个接口只去约束一个能力,让一个类型同时去实现多个接口
interface Eat {
eat (food: string): void
}
interface Run {
run (distance: number): void
}
// 不同的类型,实现相同的接口
class Person implements Eat, Run {
eat (food: string): void {
console.log(`优雅的进餐:${food}`)
}
run (distance: number) {
console.log(`直立行走:${distance}`)
}
}
class Animal implements Eat, Run {
eat (food: string): void {
console.log(`呼噜呼噜的吃:${food}`);
}
run (distance: number) {
console.log(`爬行:${distance}`)
}
}
抽象类
类似于接口,用来约束子类当中必须要有某一个成员。但与接口不同的是,抽象类可以包含一些具体的实现,而接口只能是某一个成员的抽象,不包括具有的实现。
// 抽象类只能被继承,不能再使用 new 进行实例化
abstract class Animal {
eat (food: string): void {
console.log(`呼噜呼噜的吃:${food}`)
}
// 抽象方法, 不需要方法体
abstract run (distance: number): void
}
// 当父类中存在抽象方法时,子类必须去实现抽象方法
class Dog extends Animal {
run(distance: number): void {
console.log('四脚爬行', distance);
}
}
// 子类创建实例时,会同时拥有父类中的方法,以及自身所实现的方法
const d = new Dog()
d.eat('嗯')
d.run(1)
泛型
概述
泛型就是在声明函数时不去指定具体的类型,等到在调用的时候再去传递具体的类型。
目的
极大程度的去复用代码。
function createNumberArray (length: number, value: number): number[] {
// Array 默认创建的是 any类型的数组,因此需要使用泛型进行指定,传递一个类型
return Array<number>(length).fill(value)
}
function createStringArray (length: number, value: string): string[] {
return Array<string>(length).fill(value)
}
// 不明确的类型,使用 T 替换,调用时传入
function createArray<T> (length: number, value: T): T[] {
return Array<T>(length).fill(value)
}
// const res = createNumberArray(3, 100) // res => [100, 100, 100]
const res = createArray<string>(3, 'foo')
总结
泛型就是定义时不能明确的类型变成一个参数,让我们在使用的时候再去传递的类型参数。
类型声明
作用
兼容普通的js代码。
下面以 lodash 为例,手动声明函数类型
// 此时,只是安装了 'lodash' 开发依赖
import { camelCase } from 'lodash'
// 需要使用 declare 关键字,手动声明函数类型
declare function camelCase(input: string): string
const res = camelCase('hello typed') // 不手动声明,会报错
- 安装类型声明模块
yarn add @types/lodash --dev
代码如下(示例):
import { camelCase } from 'lodash'
const res = camelCase('hello typed')
类型声明模块介绍
类型声明模块没有实际的代码,只是对对应的模块做一些类型声明。若模块中已经存在类型声明,则不需要引入类型声明模块。
@types/模块名 // 形式
作用域问题
在其他文件中,变量或函数等已经在 全局作用域 中定义,再进行定义的话,就会产生命名冲突,编译时会报错。
解决方案
- 使用 匿名函数,立即执行,生成单独的作用域
(function () {
const a = 123
})()
- 使用 export {},利用导出模块的概念,生成作用域
// 下面的 {} 并不是指导出空对象,而是 export 的语法
// 利用下面的语法,可以使这个文件中的成员变成这个模块当中的局部成员
// 一般不会用,实际开发中,就会以模块的形式进行开发
export {}
const a = 123
Polyfill
TS 对于语言的编译,只是语法层面,如果是 API 层面的的补充,需要手动 Polyfill!
core-js
core-js 基本上把能 polyfill API 都实现了。但是,属于手动引入 Polyfill。
- 1,安装插件模块
$ npm i core-js --save
- 2,使用core-js,两种引入方式,建议按需引入
引入语法如下:
// 一, 全部引入
import 'core-js'
// 二, 按需引入
import 'core-js/features/object'
- 3,测试是否成功
$ nvm use 12 # 12 node version, compile ts
$ tsc # compile ts
$ nvm use 0 # 0 node version
$ node xxx.ts # success or failure
- 无法实现 Polyfill
Object.defineProperty 完全无法 Polyfill
Promise 微任务,用宏任务代替
Babel
使用 Babel 自动化的 Polyfill。
- 1,安装依赖模块
$ npm i @babel/cli @babel/core @babel/preset-env @babel/preset-typescript --save
模块注释
@babel/cli:babel 的命令行入口
@babel/core:babel的核心模块
@babel/preset-env:是一个包含ES新特性所有转换插件的集合,可以根据环境判断哪些转哪些不转
@babel/preset-typescript:是一个包含typescript转换为ES的插件集合
- 2,配置babel.config.js文件
配置代码如下:
// JSDoc
// @ts-check
/** @type {import('@babel/core').ConfigAPI} */
module.exports = {
presets: [
[
'@babel/env',
{
useBuiltIns: 'usage',
corejs: {
version: 3
}
}
],
'@babel/typescript' // 不会做 TS 语法检查,只是移除类型注解
]
}
- 3,使用编译命令
$ npx babel source.ts -o output.js # source.ts 源文件 output.js 编译后的文件