- TypeScript 的好处
- 基础类型
- 接口,类型alias,类
- 模块结构
- 配置
- TypeScript lint
- 代码格式化
技术前提
TypeScript 带来了什么
TypeScript解决了JavaScript代码增长带来难于阅读和难于维护的痛点。比起JavaScript,
- 开发前期可以捕获代码错误
- 静态类型允许开发工具提升开发者的经验和生产力
- 兼容各种浏览器,以及一些非浏览器平台
基础类型
原生类型
string
: Unicode字符串
number
: 表达整数和浮点数
boolean
: 逻辑true或false
undefined
: 未定义值
null
: null
类型标注
TypeScript在变量声明时带有类型,语法为:Type
。
类型推断
TypeScript可以简单地有赋值推断出其类型,
Any
既没有值,也没有指定类型的变量,它的类型是any
,
它用于动态声明,表示其值的类型会随后被确定。
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; }
|
因此在方法调用时,可以不传参数,
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();
|
抽象方法需要带有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中有关键字get
和set
,语法和方法类似,其中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; ... } }
|
静态作用域里面的访问成员要求也必须是静态的,因此不能在静态方法中使用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; } }
|
打开终端,输入如下命令,
不出错的话,在对应目录会生成一个文件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
: 告诉编译器如何处理模块,有两个选项classic
或node
,如果不指定,默认是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官网。
代码格式化有很多种,常见的是prettier
,另外还有less、css等等。
本章回归
- TS的5中原生类型是那些? // string, number, boolean, undefined, null
- 下面变量
flag
类型推断的类型是什么? // boolean
- 接口和类型别名的区别是什么? // type alias不能继承
- 下面代码有什么错误?如何处理?
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方法;
- 如果想要我们的TS支持IE11,编译选项
--target
应该带什么?
- 如何转换为ES6版本的
.js
文件? // --target ES6
- 如何阻止代码中出现
console.log
语句? // tslint.yml 带no-console