(一) 基础类型
什么是TypeScript
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持
官方定义
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 是开源的。
为什么要学习 TypeScript
-
Typescript 是对 JavaScript 的增强,它大大增强了代码的可读性和维护性,让我们编写出更加健壮的代码
-
未来前端的发展趋势
- 最新发布的 Vue3 使用了TypeScript。
- Angular 在 2.0 版本就内置了 TypeScript
- React 对TypeScript 的支持也很丝滑
开始学习TypeScript
类型注解
学习TypeScript之前我们先来了解类型注解这个概念TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。
// js
let num = 5
num = 'hello' // 没毛病
// ts
let num: number = 5
num = 'hello' // 报错,因为定义了num为number类型的变量所以赋值为string类型时会报错
复制代码
然后我们来看看TypeScript中的基本类型
-
布尔类型(boolean)
-
数字类型(number)
-
字符串类型(string)
-
数组类型(array)
-
元组类型(tuple)
-
枚举类型(enum)
-
任意值类型(any)
-
null 和 undefined
-
void类型
-
never类型
-
object 类型
-
类型断言
-
Symbol 类型
布尔值
最基本的数据类型就是简单的true
/false
值,在JavaScript和TypeScript里叫做boolean
(其它语言中也一样)。
let bool: boolean = true
复制代码
数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是number
。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let num: number = 123
num = 0b1101 // 二进制
num = 0o164 // 八进制
num = 0x8b // 十六进制
复制代码
字符串类型
和JavaScript一样,可以使用双引号("
)或单引号('
)表示字符串。
let name: string = 'hui'
复制代码
你还可以使用_模版字符串_,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( **** ),并且以
${ expr }`这种形式嵌入表达式
let name: string = `hui`
let hello: string = `hello, my name is ${name}`
复制代码
数组
TypeScript像JavaScript一样可以操作数组元素。
有两种方式可以定义数组。 第一种,可以在元素类型后面接上[]
,表示由此类型元素组成的一个数组:
let arr1: number[] = [1, 2, 3]
let arr2: string[] = ['a', 'b', 'c']
let arr3: (number | string)[] = [1, 'b', 3] // 数组元素既可以是number类型也可以是string类型
复制代码
第二种方式是使用数组泛型,Array<元素类型>
:
let arr1: Array<number> = [1, 2, 3]
let arr2: Array<string> = ['a', 'b', 'c']
let arr3: Array<number | string> = [1, 'b', 2]
复制代码
元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string
和number
类型的元组。
let x: [string, number]
x = ['hui', 2] // OK
x = [2, 'hui'] // Error
复制代码
枚举类型
enum
类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Roles {
SUPER_ROLE,
ADMIN,
USER
}
console.log(Roles.SUPER_ROLE) // 0
console.log(Roles.ADMIN) // 1
console.log(Roles.USER) // 2
复制代码
默认情况下,从0
开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从1
开始编号:
enum Roles {
SUPER_ROLE = 1,
ADMIN,
USER
}
console.log(Roles.SUPER_ROLE) // 1
console.log(Roles.ADMIN) // 2
console.log(Roles.USER) // 3
复制代码
任意值 any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用any
类型来标记这些变量:
let name:any = 'hui'
name = 123 // OK
name = true // OK
复制代码
当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, 'hui']
复制代码
尽量少的使用
any
, 否则你可能在用 AnyScript 写代码
空值
某种程度上来说,void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是void
:
function getName():void {
alert('my name is hui')
}
复制代码
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
let v: void
v = undefined // OK
复制代码
Null 和 Undefined
TypeScript里,undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 和void
相似,它们的本身的类型用处不是很大:
let u: undefined = undefined
let n: null = null
复制代码
Never
never
类型表示的是那些永不存在的值的类型。never
类型用于返回 error
死循环
function getError(message: string): never {
throw new Error(message);
}
function infiniteFunc(): never {
while (true) {}
}
复制代码
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,_没有_类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使any
也不可以赋值给never
。
let neverVarible: never = (() => {
while (true) {}
})()
let num: number = 123
let name: string = 'hui'
num = neverVarible // OK
neverVarible = name // error
复制代码
类型断言
类型断言就是手动指定一个类型的值
类型断言有两种形式。 其一是“尖括号”语法:
let str: any = "this is a string";
let strLength: number = (<string>str).length;
复制代码
另一个为as
语法:
let str: any = "this is a string";
let strLength: number = (str as string).length;
复制代码
(二) 接口
什么是接口(interface)
TypeScript的核心原则之一是对值所具有的_结构_进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
基本介绍
JavaScript 中定义一个函数,用来获取一个用户的姓名和年龄的字符串:
const getUserInfo = function(user) {
return name: ${user.name}, age: ${user.age}
}
复制代码
函数调用:
getUserInfo({name: "koala", age: 18})
复制代码
这对于我们之前在写 JavaScript 的时候,再正常不过了,但是如果这个 getUserInfo
在多人开发过程中,如果它是个公共函数
,多个开发者都会调用,如果不是每个人点进来看函数对应注释
,可能会出现以下问题:
// 错误的调用
getUserInfo() // Uncaught TypeError: Cannot read property 'name' of undefined
console.log(getUserInfo({name: "kaola"})) // name: kaola, age: undefined
getUserInfo({name: "kaola", height: 1.66}) // name: koala, age: undefined
复制代码
JavaScript 是弱类型的语言,所以并不会对我们传入的代码进行任何的检测,有些错你自己都说不清楚,但是就出了问题。
TypeScript 中的 interface 可以解决这个问题
const getUserInfo = (user: {name: string, age: number}): string => {
return `name: ${user.name} age: ${user.age}`;
};
复制代码
正确的调用是如下的方式:
getUserInfo({name: "kaola", age: 18});
复制代码
如果调用者出现了错误的调用,那么 TypeScript
会直接给出错误的提示信息:
// 错误的调用
getUserInfo(); // 错误信息:An argument for 'user' was not provided.
getUserInfo({name: "coderwhy"}); // 错误信息:Property 'age' is missing in type '{ name: string; }'
getUserInfo({name: "coderwhy", height: 1.88}); // 错误信息:类型不匹配
复制代码
这时候你会发现这段代码还是有点长,代码不便与阅读,这时候就体现了 interface
的必要性。
使用 interface 对 user 的类型进行重构。
我们先定义一个 IUser
接口:
// 先定义一个接口
interface IUser {
name: string;
age: number;
}
复制代码
接下来我们看一下函数
如何来写:
const getUserInfo = (user: IUser): string => {
return `name: ${user.name}, age: ${user.age}`;
};
// 正确的调用
getUserInfo({name: "koala", age: 18});
复制代码
// 错误的调用和之前一样,报错信息也相同不再说明。
接口中函数的定义再次改造
定义两个接口:
type IUserInfoFunc = (user: IUser) => string;
interface IUser {
name: string;
age: number;
}
复制代码
接着我们去定义函数和调用函数即可:
const getUserInfo: IUserInfoFunc = (user) => {
return `name: ${user.name}, age: ${user.age}`;
};
复制代码
// 正确的调用
getUserInfo({name: "koala", age: 18});
复制代码
// 错误的调用
getUserInfo();
复制代码
接口的名字首字母要大写
可选属性
接口里的属性有时候不都是必须的,有时候是可选的,我们用 ?
用来定义可选属性
interface Fruit {
type: string,
color?: string
}
const getFruit({type, color}: Fruit):string {
return `A ${color ? (color + ' ') : ''} ${type}`
}
getFruit({
color: 'red',
type: 'apple'
}) // ok
getFruit({
type: 'apple'
}) // ok
复制代码
多余属性检查
还是上面那个例子,如果我们在传递的参数中多加一个属性,例如
getFruit({
color: 'red',
type: 'apple',
size: 19
}) // err
复制代码
这个时候 TypeScript 会告诉我们传的参数多了一个 size
的属性,但是其实这个是不影响我们的结果的,这个时候有两种办法来解决这个问题。
第一种使用类型断言
getFruit({
color: 'red',
type: 'apple',
size: 19
} as Fruit) // ok
复制代码
第二种使用索引签名
interface Fruit {
type: string,
color?: string,
[prop: string]: any
}
const getFruit({type, color}: Fruit):string {
return `A ${color ? (color + ' ') : ''} ${type}`
}
getFruit({
color: 'red',
type: 'apple',
size: 19
} as Fruit) // ok
复制代码
只读属性
接口还可以设置只读属性,表示这个属性不可被修改
interface Fruit {
type: string,
readonly color: string
}
const apple: Fruit = {
type: 'apple',
color: 'red'
}
apply.color = 'green' // err
复制代码
函数类型
接口不仅可以定义对象的形式,还可以定义函数形式
interface AddFunc {
(number1: number, number2: number) => number
}
const addFunc: AddFunc = (n1, n2) => {
return n1 + n2
}
复制代码
索引类型
接口还定义索引类型
interface ArrInter {
0: string,
1: number
}
const arr: ArrInter = ['a', 1]
复制代码
继承接口
一个接口可以继承另一个接口, 使用 extends
关键字来实现继承
interface Fruit {
type: string
}
interface Apple extends Fruit {
color: string
}
cosnt apple: Apple = {
color: 'red'
} // 报错, 因为继承 Fruit 接口所以必须有 type 属性
复制代码
混合类型接口
接口还可以定义包含任意类型属性的接口
interface Counter {
(): void, // 一个函数
count: number
}
const getCounter = ():Counter => {
const c = () => c.count++
c.count = 1
return c
}
const counter: Counter = getCounter()
counter()
console.log(counter.count) // 2
counter()
console.log(counter.count) // 3
counter()
console.log(counter.count) // 4
复制代码
(三) 类
类
基本例子
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
复制代码
我们声明了一个Greeter类,它包含三个成员:一个greeting属性,一个greet方法,一个构造函数。最后一行,我们使用new构造了Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。
什么是类
在ES5中,只有对象的概念,对象是通过构造函数创建出来的,构造函数本身也是对象。所以说JavaScript是一种基于对象的语言。
ES6之后,有了class,也就是我们的类,虽然class实质上是 JavaScript 现有的基于原型的继承的语法糖。但我们同样可以借鉴C#语言的面向对象的思想去理解它。让我们够使用==基于类的面向对象的方式==。
类就是对象的设计图纸,上面记录着一个事物应该有哪些字段,哪些方法,但类不是个对象,只是对象的定义。通过new 一个类,我们才能得到一个对象,这个对象有类上描述的所有成员。
这就是基于类的面向对象的方式。程序员画好了许许多多的设计图(类)。然后new 它们,形成了一个一个的对象,对象之间彼此关联,协作。我们的程序就这样运行了。
所以我们有了类的概念,通晓类与对象的关系后,这会影响我们的编程思维方式,需要什么东西,先定义个类,在new出个对象,让其工作。我们的对象更加具象了,它有类给予它的定义。
定义类
class Person {
}
类中的成员
类内的成员有:字段、存取器、构造函数、方法。
字段
class Person {
age: number; // 这就是一个字段
}
类中的变量成员。可以被实例化的对象设置和读取。也可以被类内其他成员设置和读取。
存取器
class Person {
private _age: number;
get age(): number {
return this._age;
}
set age(newName: number) {
if(newName > 150){
this._age = 150;
}
this._age = newAge;
}
}
这里的get/set方法,就是_age变量的存取器。首先我们为_age变量添加一个private修饰符。表示它是个私有变量,禁止外部对它的访问。
const person = new Person();
person._age // 这句会报错,TS不允许我们读取私有变量。
要想使用_age,需要利用存取器:
const person = new Person();
person.age = 15; // 使用set age方法
const age = pereson.age; // 使用get age方法
使用存取器的好处是对变量的访问和设置,有了控制。原则上类内部的变量,外部是不能访问的,这就是封装性。要想访问,只能通过存取器方法。
构造函数
class Person {
private _age: number;
constructor(age: number) {
this._age = age;
}
}
constructor就是类的构造器,他的使用如下:
const person = new Person(15);
构造器就是我们在new一个类的时候,调用的方法。
方法
JavaScript中,都叫函数,但是有了类之后,类中的函数成员,就叫方法。
class Person {
eat() {
....
}
drink() {
}
}
这里的eat和drink,就是Person类的方法。
this关键字
和原生js中捉摸不定的this不同,类中的this,非常好理解,它表示当前类的实例。
继承
类允许继承,基本例子
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
复制代码
在这个例子中,类从基类中继承了属性和方法,在这里,Dog是一个派生类,派生自Animal基类,派生类通常被称作子类,基类通常被称作超类。 复杂例子
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Horse extends Animal {
constructor(name: string) {
// 必须先调用super函数
super(name);
}
// 重写父类的方法
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let tom: Animal = new Horse("Tommy the Palomino");
tom.move(34);
复制代码
在这个例子中,子类中多了构造函数,当子类中包含构造函数时,必须 要先调用super(),它会执行超类的构造函数,并且一定要在构造函数访问this之前。子类还可以重写父类的方法。 所以,tom虽然被声明为animal类型,但它的值时House,所以执行的move方法是House中的move方法。
Galloping...
Tommy the Palomino moved 34m.
复制代码
公共,私有与受保护的修饰符
public
默认为public,不需要特别去标记
private
私有属性不可以在类的外部被访问。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
复制代码
protected
protected属性与私有属性比较相似,不同的是,protected可以在派生类中被访问,
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
this.department = department;
}
public getElevatorPitch() {
// 派生类可以访问父类中的protected属性
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误,protected属性不可在类的外部访问。
复制代码
readonly
只读属性,只读属性必须在声明时或构造函数里被初始化
参数属性
class Animal {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
复制代码
将原来需要在构造函数声明之前定义的属性直接在构造函数里使用private name: string
参数来创建和初始化name成员,把声明和赋值合并至一处。
存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
复制代码
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly。
静态属性
静态属性是存在于类本身的成员,而不是在类的实例中,不需要在类的实例化时才被初始化。 访问静态属性,需要在属性名前加上类名,如同在实例属性上使用this.前缀来访问属性一样。
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
复制代码
抽象类
抽象类一般是其他派生类的基类,一般不会实例化。 abstract 关键字主要是用于定义抽象类和抽象类中的方法,它的语法与接口的语法很相似,两者都是定义方法签名但不包含方法体。 不同于接口的是:抽象类中的抽象方法不包含具体实现但必须在派生类中实现;抽象类可以包含成员的实现细节;抽象方法必须包含abstract关键字并且可以包含访问修饰符。
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}
// 定义一个抽象基类中没有声明的方法,但是它没办法在实例中使用
generateReports(): void {
console.log('Generating accounting reports...');
}
}
let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在
复制代码
高级类
构造函数
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的实例的类型。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// 声明greeter类的实例的类型是Greeter
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
复制代码
这里,我们写了let greeter: Greeter,意思是Greeter类的实例的类型是Greeter。
我们也创建了一个叫做构造函数的值。 这个函数会在我们使用new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
复制代码
上面的代码里,let Greeter将被赋值为构造函数。 当我们调用new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有实例部分与静态部分这两个部分。
(四) 开始使用 TypeScript
在开始使用 TypeScript 前你最好有以下准备:
- Node.js > 8.0,最好是最新的稳定版(目前是V10.16.3 )
- 一个包管理工具 npm 或者 yarn
- 一个文本编辑器或者 IDE (笔者的是 vscode)
相关的 shell 命令仅适用于 *nix 系统,windows 系统不适用
安装 TypeScript
TypeScript 的安装很简单,你可以通过npm直接在全局安装 TypeScript。
> npm install -g typescript
创建环境
随后我们要创建一个目录:
mkdir ts-study && cd ts-study
接着创建 src 目录:
mkdir src && touch src/index.ts
接着我们用npm将目录初始化:
npm init
此时我们要使用 TypeScript 的话通常也需要初始化:
tsc --init
这个时候你会发现目录下多了一个tsconfig.json
文件.
这是 TypeScript 的配置文件,里面已经包含官方初始化的一些配置以及注释,我们现在进行自定义的配置:
{
"compilerOptions": {
"target": "es5", // 指定 ECMAScript 目标版本: 'ES5'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"moduleResolution": "node", // 选择模块解析策略
"experimentalDecorators": true, // 启用实验性的ES装饰器
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
"sourceMap": true, // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"alwaysStrict": true, // 以严格模式检查模块,并在每个文件里加入 'use strict'
"declaration": true, // 生成相应的.d.ts文件
"removeComments": true, // 删除编译后的所有的注释
"noImplicitReturns": true, // 不是函数的所有返回路径都有返回值时报错
"importHelpers": true, // 从 tslib 导入辅助工具函数
"lib": ["es6", "dom"], // 指定要包含在编译中的库文件
"typeRoots": ["node_modules/@types"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [ // 需要编译的ts文件一个*表示文件匹配**表示忽略文件的深度问题
"./src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
]
}
然后在package.json中加入我们的script命令:
{
"name": "ts-study",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"build": "tsc", // 编译
"build:w": "tsc -w" // 监听文件,有变动即编译
},
"author": "",
"license": "ISC",
"devDependencies": {
"typescript ": "^3.6.4"
}
}
编写第一个 TypeScript 程序
在src/index.ts
中输入以下代码:
function greeter(person) {
return "Hello, " + person
}
const user = "Jane User"
这个时候你会看到一个警告,这个警告在官方默认配置中是不会出现的,正是由于我们开启了 noImplicitAny
选项,对于隐式含有 any
类型的参数或者变量进行警告⚠️.
之所以一开始就开启严格模式,是因为一旦你开始放任any
类型的泛滥,就会把 TypeScript 变成 AnyScript ,会很难改掉这个恶习,所以从一开始就要用规范的 TypeScript 编码习惯。
我们进行修改如下:
function greeter(person: string) {
return "Hello, " + person
}
此时我们可以看到,greeter
函数自动加上了返回值类型,这是 TypeScript 自带的_类型推导_。