第一章 基础

  • TypeScript 的好处
  • 基础类型
  • 接口,类型alias,类
  • 模块结构
  • 配置
  • TypeScript lint
  • 代码格式化

技术前提

  • TypeScript 背景: https://www.typescriptlang.org/play/

  • Node.js 以及npm:Node >= 8.2, npm >= 5.2

  • TypeScript安装:

    1
    npm install -g typescript
  • Visual Studio Code:前端开发利器

TypeScript 带来了什么

TypeScript解决了JavaScript代码增长带来难于阅读和难于维护的痛点。比起JavaScript,

  • 开发前期可以捕获代码错误
  • 静态类型允许开发工具提升开发者的经验和生产力
  • 兼容各种浏览器,以及一些非浏览器平台

基础类型

原生类型

  • string: Unicode字符串
  • number: 表达整数和浮点数
  • boolean: 逻辑true或false
  • undefined: 未定义值
  • null: null

类型标注

TypeScript在变量声明时带有类型,语法为:Type

类型推断

TypeScript可以简单地有赋值推断出其类型,

1
let flag = false;

Any

既没有值,也没有指定类型的变量,它的类型是any

1
let flag;

它用于动态声明,表示其值的类型会随后被确定。

Void

用于函数的返回表示,

1
2
3
function logText(text: string): void {
console.log(text);
}

通常不带return语句体的函数,返回值类型会自动推断为void

Never

表示“从不”,用于指定该代码不可达

1
2
3
4
5
function foreverTask(taskName: string): never {
while(true) {
console.log(`Doing ${taskName} over and over again ...`);
}
}

该函数一直循环,永不返回,所以需要给定类型never

Enumerations

1
2
3
4
5
6
enum OrderStatus {
Paid,
Shipped,
Completed,
Cancelled
}

下标访问,

1
let status = OrderStatus.Paid

TypeScript的枚举,遵循自动下标的语法,即

1
2
3
4
5
6
enum OrderStatus {
Paid = 1,
Shipped,
Completed,
Cancelled
}

不声明的部分按顺序逐个递增,

1
2
let status = OrderStatus.Shipped;
console.log(status); // print 2

Objects

TS的object和JS是共享的,是一种非原生类型。例如,

1
2
3
4
5
const customer = {
name: "Lamps Ltd",
turnover: 2000134,
active: true
};

和JS一样,可以通过下标,直接修改和访问其值

1
customer.turnover = 123,

不同的是,它有类型,所以下面会报错

1
customer.turnover = "500,500",

Arrays

数组需要带类型,其它地方用法和JS差不多

1
2
const numbers: number[] = [];
number.push(1);

另外可以通过类型推断来声明,

1
const numbers = [1, 3, 5];

迭代方式有几种,

1
2
3
for (let i in numbers) {
console.log(numbers[i]);
}

或者,

1
2
3
numbers.forEach(function (num){
console.log(num);
});

创建接口,类型别名,类

常量的定义,

1
2
3
4
5
const customer = {
name: "Lamps Ltd",
turnover: 2000134,
active: true
}

但是改为下面会出现编译错误,

1
2
3
4
5
6
7
let customer: Object;
customer = {
name: "Lamps Ltd",
turnover: 2000134,
active: true
};
customer.turnover = 20002000; // error

Typescript 编译器不知道customer有哪些属性,所以需要引入结构化特性。

Interfaces

接口用interface关键字声明,

1
2
3
interface Product {
...
}

Properties

结构的属性,

1
2
3
4
interface Product {
name: string;
unitPrice: number;
}

接口的属性必须声明了才能访问,

1
2
3
4
const table: Product = {
name: "Table",
unitPrice: 500
}

接口也是类型,所以可以在其它接口引用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Product {
name: string;
unitPrice: number;
}

interface OrderDetail {
product: Product;
quantity: number;
}

const table: Product = {
name: "Table",
unitPrice: 500
}

const tableOrder: OrderDetail = {
product: table,
quantity: 1
};

方法签名

接口可以包含方法签名而没有具体实现,

1
2
3
4
5
interface OrderDetail {
product: Product;
quantity: number;
getTotal(idscount: number): number;
}

具体对象需要实现接口的方法签名,方法签名必须一致,

1
2
3
4
5
6
7
8
9
const tableOrder: OrderDetail = {
product: table,
quantity: 1,
getTotal(discount: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity;
const discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
}
};

接口中的方法签名,参数部分可以不用声明类型,

1
2
3
4
interface OrderDetail {
....
getTotal(number): number;
}

但省略的参数类型使得阅读难于理解,我们不知道具体参数是什么类型?

可选属性,可选参数

和大部分现代语言类似,TypeScript中使用?表示属性或参数是个optional 的。

1
2
3
4
5
6
interface OrderDetail {
product: Product;
quantity: number;
dateAdded?: Date,
getTotal(discount: number): number;
}

方法签名的参数也可以是可选的,

1
2
3
4
5
6
interface OrderDetail {
product: Product;
quantity: number;
dateAdded?: Date,
getTotal(discount?: number): number;
}

方法签名的实现可以改为,

1
2
3
4
5
getTotal(discount?: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity;
const discountAmount = priceWithoutDiscount * (discount || 0);
return priceWithoutDiscount - discountAmount;
}

因此在方法调用时,可以不传参数,

1
tableOrder.getTotal();

Readonly 属性

readonly属性,顾名思义只能读取,不能修改,

1
2
3
4
interface Product {
readonly name: string;
unitPrice: number;
}

因此,下面操作发生编译错误,

1
2
3
4
5
6
const table: Product = {
name: "Table";
unitPrice: 500
};

table.name = "Better Table";

接口继承

接口继承使用extends关键字,

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Product {
name: string;
unitPrice: number;
}

interface DiscountCode [
code: string;
percentage: number;
}

interface ProductWithDiscountCodes extends Product {
discountCodes: DiscountCode[];
}

接口的实例可以简单创建,

1
2
3
4
5
6
7
8
const table: ProductWithDiscountCodes = {
name: "Table",
unitPrice: 500,
discountCodes: [
{ code: "SUMMER10", percentage: 0.1 },
{ code: "BFRI", percentage: 0.2 }
]
};

类型别名

类型别名就是给指定类型标准一个新的类型

1
2
3
4
5
6
7
type GetTotal = (discount: number) => number;

interface OrderDetail {
product: Product;
quantity: number;
getTotal: GetTotal;
}

类型别名和接口相似,不同的是类型别名不能有extends,也不能implemented。

相比接口,类有更多的特性,

基础类

1
2
3
4
class Product {
name: stirng;
unitPrice: number;
}

大部分概念和Java类型,可以通过new关键字声明实例

1
2
3
const table = new Product();
table.name = "Table";
table.unitPrice = 500;

可以调用对应的成员方法,

1
2
3
4
5
6
7
8
9
10
class OrderDetail {
product: Product;
quantity: number;

getTotal(discount: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity;
const discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
}
}

创建对应实例,调用成员方法

1
2
3
4
5
6
7
8
9
10
const table = new Product();
table.name = "Table";
table.unitPrice = 500;

const orderDtail = new OrderDetail();
orderDetail.product = table;
orderDetail.quantity = 2;

const total = orderDetail.getTotal(0.1);
console.log(total);

接口继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IOrderDetail {
product: Product;
quantity: number;
getTotal(discount: number): number;
}

class OrderDetail implements IOrderDetail {
product: Product;
quantity: number;

getTotal(discount: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity;
const discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
}
}

构造器

TS中的构造函数概念和Java一样,形式不一样,TS中需要使用constructor私有成员,

1
2
3
4
5
6
7
8
9
10
11
12
13
class OrderDetail implements IOrderDetail {
product: Product;
quantity: number;

constructor(product: Product, quantity: number) {
this.product = product;
this.quantity = quantity;
}

getTotal(discount: number): number = {
...
}
}

创建实例时,会强制要求传递参数,

1
const orderDetail = new OrderDetail(table, 2);

某些情况下,可以使用默认值,

1
2
3
4
constructor(product: Product, quantity: number = 1) {
this.product = product;
this.quantity = quantitty;
}

声明实例时可以不写,

1
const orderDetail = new OrderDetail(table);

可以少写点代码,在构造参数前引入public关键字,

1
2
3
4
5
6
7
8
9
10
class OrderDetail implements IOrderDetail {
constructor(public product: Product, public quantity: number = 1) {
this.product = product;
this.quantity = quantity;
}

getTotal(discount: number): number {
...
}
}

类继承

类之间可以继承,使用关键字extends

1
2
3
4
5
6
7
8
9
10
11
12
13
class Product {
name: string;
unitPrice: number;
}

interface DiscountCode {
code: string;
percentage: number;
}

class ProductWithDiscountCodes etends Product {
discountCodes: DiscountCode[];
}

该类型实例的创建如下,

1
2
3
4
5
6
7
const table = new ProductWithDiscountCodes();
table.name = "Table";
table.unitPrice = 500;
table.discountCodes = [
{ code: "SUMMER10", percentage: 0.1 },
{ code: "BFRI", percentage: 0.2 }
];

父类包含构造函数,子类也必须包含构造函数的实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Product {
constructor(public: name: string; public unitPrice: number) {
}
}

interface DiscountCode {
code: string;
percentage: number;
}

class ProductWithDiscountCodes extends Product {
constructor(public name: string, public unitPrice: number) {
super(name, unitPrice);
}
discountCodes: DiscountCode[];
}

抽象类

抽象类使用abstract关键字声明,表示没有实例化能力的成员

1
2
3
4
5
6
abstract class Product {
name: string;
uitPrice: number;
}

const bread = new Procut(); // compile error

抽象方法需要带有abstract关键字,

1
2
3
4
5
abstract class Product {
name: string;
uitPrice: number;
abstract delete(): void;
}

所有子类都要实现这个抽象方法,

1
2
3
4
5
6
7
8
9
class Fond extends Product {
deleted: boolean;
constructor(public bestBefore: Date) {
super();
}
delete() {
this.deleted = false;
}
}

访问修改器

按照访问作用域划分,目前有以下几种,

  • public:
  • private:
  • protected:
  • :

Setter和Getter

和Java不同,TS中有关键字getset,语法和方法类似,其中get不带参数;set带一个参数,一般用于私有方法的操作处理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Product {
name: string;

private _unitPrice: number;
get unitPrice(): number {
return this._unitPrice || 0;
}

set unitPrice(value: number) {
if(value < 0) {
value = 0;
}
this._unitPrice = value;
}
}

const table = new Product();
table.name = "Table";
console.log(table.unitPrice);
table.unitPrice = -10;
console.log(table.unitPrice);

Static

静态声明的方法或属性,作用于类自身而不是类的对象实例。

1
2
3
4
5
6
7
8
class OrderDetail {
product: Product;
quantity: number;
static getTotal(discount: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity; // compile error
...
}
}

静态作用域里面的访问成员要求也必须是静态的,因此不能在静态方法中使用this.properties进行访问

1
2
3
4
5
6
7
8
static getTotal(unitPrice: number, quantity: number, discount: number): number {
const priceWithoutDiscount = unitPrice * quantity;
...
return ...;
}

const total = OrderDetail.getTotal(500, 2, 0.1);
console.log(total);

结构化 转变 为模块

由于TypeScript是最终编译成为JavaScript,并且其作用域的是全局的。这样就带来一个问题是,同名的条目会在不同文件中造成冲突,因此需要实现模块化来解决这个问题,使得代码更容易组织,更高的重用性。

模块化格式

模块化是属于ES6的JavaScript的部分特性。简要描述一下TypeScript的不同模块化格式:

  • AMD(Asynchronous Module Definition): 最常见,对目标浏览器,用一个define函数来定义模块。
  • CommonJS: 用于Node.js程式,使用module.exports来定义模块,用require来定义依赖。
  • UMD(Universal Module Definition): 可以用于浏览器app和Node.js程式。
  • ES6: 使用export关键字来定义模块,import来定义依赖。

笔者这里使用ES6。

Exporting

从一个module进行export以允许在其它module中被使用。使用export关键字。

1
2
3
4
export interface Product {
name: string;
unitPrice: number;
}

或者重命名,

1
2
3
4
5
6
interface Product {
name: string;
unitPrice: number;
}

export { Product as Stock }

Importing

有export就需要在其它模块进行import,

1
2
3
4
5
6
7
8
9
import { Product } from "./product";

class OrderDetail {
product: Product;
quantity: number;
getTotal(discount: number): number {
...
}
}

或者重命名,

1
2
3
4
5
6
import { Product as Stock } from "./product";

class OrderDetail {
product: Stock;
...
}

default exports

带有default 语句的export不需要花括号,

1
2
3
4
5
6
export default interface {
name: string;
unitPrice: number;
}

import Product from "./product";

编译配置

TypeScript的编译器是tsc,它会将对应的TS文件编译为JS文件,

新建文件,orderDetail.ts,内容如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface Product {
name: string;
unitPrice: number;
}

export class OrderDetail {
product: Product;
quantity: number;
getTotal(discount: number): number {
const priceWithoutDiscount = this.product.unitPrice * this.quantity;
const discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
}
}

打开终端,输入如下命令,

1
tsc orderDetail

不出错的话,在对应目录会生成一个文件orderDetail.js,里面是转换的JavaScript内容,

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
exports.__esModule = true;
var OrderDetail = (function () {
function OrderDetail() {
}
OrderDetail.prototype.getTotal = function (discount) {
var priceWithoutDiscount = this.product.unitPrice * this.quantity;
var discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
};
return OrderDetail;
}());
exports.OrderDetail = OrderDetail;

常见编译选项

  • --target: 目标,默认是ES3
  • --outDir: 输出目录
  • --module: 指定模块格式,--target是ES3或ES5时,默认是CommonJS
  • --allowJS: 告诉编译器处理JavaScript文件,应对那些TypeScript无法应对时候,需要JavaScript的情况。
  • --watch: watch mode模式,修改文件后保存后立即编译输出。
  • --noImplicitAny: 强制显式指定any类型。
  • --noImplicitReturns: 强制显式返回。即对于不是void返回类型,所有case必须有返回值。
  • --sourceMap: development模式中,生成*.map文件,以允许调试
  • --moduleResolution: 告诉编译器如何处理模块,有两个选项classicnode,如果不指定,默认是classic,会要求使用第三方包,所以需要显式设置为node,编译器会查找node_modules模块。

tsconfig.json

tsconfig.json文件是上面这些开关的几种配置文件,样例如下,

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"target": "esnext",
"outDir": "dist",
"module": "es6",
"moduleResolution": "node",
"sourceMap": true,
"noImplicitReturns": true,
"noImplicitAny": true
}
}

指定编译文件

tsconfig.json中可以指定编译那些TypeScript文件,

1
2
3
4
5
6
{
"compilerOptions": {
...
},
"files": ["product.ts", "orderDetail.ts"]
}

或者指定目录,

1
2
3
4
5
6
{
"compilerOptions": {
...
},
"include": ["src/**/*"]
}

TypeScript linting

linting是一种检查规则,用于加强语法或优化编译,参考TSLint官网。

Code Formatting

代码格式化有很多种,常见的是prettier,另外还有less、css等等。

本章回归

  1. TS的5中原生类型是那些? // string, number, boolean, undefined, null
  2. 下面变量flag类型推断的类型是什么? // boolean
1
const flag = false;
  1. 接口和类型别名的区别是什么? // type alias不能继承
  2. 下面代码有什么错误?如何处理?
1
2
3
4
5
6
7
class Product {
constructor(public name: string, public unitPrice: number) {}
}

let table = new Product();
table.name = "Table";
table.unitPrice = 700;

// 要么编写IProduct接口,继承成员属性;要么在Product中写上成员name,unitPrice;要么带上setter/getter方法;

  1. 如果想要我们的TS支持IE11,编译选项--target应该带什么?
  2. 如何转换为ES6版本的.js文件? // --target ES6
  3. 如何阻止代码中出现console.log语句? // tslint.yml 带no-console