TS类

对于传统的 JavaScript 程序我们会使用函数基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员使用这些语法就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ES6 开始, JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。

定义

类的声明:使用class关键字

声明类的属性:在类的内部声明类的属性以及对应的类型

  • 如果类型没有声明,那么它们默认是 any 的
  • 也可以给属性设置初始化值
  • 在默认的strictPropertyInitialization模式下面我们的属性是必须 初始化的,如果没有初始化,那么编译时就会报错
    • 如果在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法
  • 类可以有自己的构造函数constructor,通过new关键字创建 一个实例时,构造函数会被调用
  • 构造函数不需要返回任何值,默认返回当前创建出来的实例
  • 类中可以有自己的函数,定义的函数称之为方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
name!: string;
age: number;

constructor(name: string, age: number) {
// this.name = name
this.age = age;
}

running() {
console.log(this.name + " running!");
}

eating() {
console.log(this.name + " eating!");
}
}

继承

  • 面向对象的其中一大特性就是继承,继承不仅仅可以减少代码量,也是多态的使用前提
  • 使用extends关键字来实现继承,子类中使用super来访问父类
  • 举例一下Student类继承自Person
    • Student类可以有自己的属性和方法,并且会继承Person的属性和方法
    • 在构造函数中,可以通过super来调用父类的构造方法,对父类中的属性进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person {
name!: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

running() {
console.log(this.name + " running!");
}

eating() {
console.log(this.name + " eating!");
}
}

class Student extends Person {
sno: number;

constructor(name: string, age: number, sno: numebr) {
super(name, age);
this.sno = sno;
}

studying() {
console.log(this.name + " studying!");
}

running() {
super.running();
console.log("Student running!");
}

eating() {
console.log("Student eating!");
}
}

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Animal {
name: string;

constructor(name: string) {
this.name = name;
}

run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`);
}
}

class Snake extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name);
}

// 重写父类型的方法
run(distance: number = 5) {
console.log("sliding...");
super.run(distance);
}
}

class Horse extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name);
}

// 重写父类型的方法
run(distance: number = 50) {
console.log("dashing...");
// 调用父类型的一般方法
super.run(distance);
}

xxx() {
console.log("xxx()");
}
}

const snake = new Snake("sn");
snake.run();

const horse = new Horse("ho");
horse.run();

/**
* 多态
* 父类型的引用指向了子类型的对象,不同类型的对象针对相同的方法,产生了不同的行为
**/

// 1.父类型引用指向子类型的实例 => 多态
const tom: Animal = new Horse("ho22");
tom.run();

/* 2.如果子类型没有扩展的方法, 可以让子类型引用指向父类型的实例 */
const tom2: Snake = new Animal("tom2");
tom2.run();
/* 3.如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
const tom3: Horse = new Animal("tom3");
tom3.run();

修饰符

在 TypeScript 中,类的属性和方法支持三种修饰符: publicprivateprotected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public,可以直接访问
  • private 修饰的是仅在同一类中可见、私有的属性或方法
  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 访问修饰符: 用来描述类内部的属性/方法的可访问性
* public: 默认值, 公开的外部也可以访问
* private: 只能类内部可以访问
* protected: 类内部和子类可以访问
**/

class Person {
protected name: string;
private age: number;
public gerder: string;

constructor(name: string, age: number, gender: string) {
this.name = name;
this.age = age;
this.gender = gender;
}

// 方法变成私有方法: 只有在类内部才能访问
private eating() {
console.log("吃东西", this.age, this.name);
}
}

const p = new Person("aaa", 18);
// console.log(p.name, p.age)
// p.name = "kobe"
// p.eating()

class Student extends Person {
constructor(name: string, age: number) {
super(name, age);
}

studying() {
console.log("在学习", this.name);
}
}

const stu = new Student("bbb", 18);

readonly

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化

1
2
3
4
5
6
7
8
9
class Person {
readonly name: string = "abc";
constructor(name: string) {
this.name = name;
}
}

const person = new Person("cba");
// person.name = 'peter' // error

参数属性

TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性

  • 这些被称为参数属性(parameter properties)
  • 可以通过在构造函数参数前添加一个可见性修饰符 publicprivateprotected或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
// 语法糖
constructor(
public name: string,
private _age: number,
readonly height: number,
protected gender: string
) {}

running() {
console.log(this._age, "eating");
}
}

const p = new Person("aaa", 18, 1.88, "male");
console.log(p.name, p.height);

// p.height = 1.98

存取器

在前面一些私有属性是不能直接访问的,或者某些属性想要监听它的获取(getter)和设置(setter)的过程,这个时候可以使用存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Person {
// 私有属性: 属性前面会使用_
private _name: string;
private _age: number;

constructor(name: string, age: number) {
this._name = name;
this._age = age;
}

running() {
console.log("running:", this._name);
}

// setter/getter: 对属性的访问进行拦截操作
set name(newValue: string) {
this._name = newValue;
}

get name() {
return this._name;
}

set age(newValue: number) {
if (newValue >= 0 && newValue < 200) {
this._age = newValue;
}
}

get age() {
return this._age;
}
}

const p = new Person("aaa", 100);
p.name = "kobe";
console.log(p.name); // "koba"

p.age = -10;
console.log(p.age); // 100

静态属性

到目前为止,只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上,使用static关键字

使用 类名.静态属性名 来访问静态属性

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 静态属性, 是类对象的属性
* 非静态属性, 是类的实例对象的属性
*/

class Person {
name1: string = "A";
static name2: string = "B";
}

console.log(Person.name2);
console.log(new Person().name1);

抽象类

继承是多态使用的前提

  • 所以在定义很多通用的调用接口时, 通常会让调用者传入父类,通过多态来实现更加灵活的调用方式
  • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,可以定义为抽象方法

抽象方法:在 TypeScript 中没有具体实现的方法(没有方法体),就是抽象方法

  • 抽象方法,必须存在于抽象类中
  • 抽象类是使用abstract声明的类

抽象类有如下的特点

  • 抽象类做为其它派生类的基类使用
  • 抽象类可以包含成员的实现细节
  • 抽象类是不能被实例的话(也就是不能通过new创建)
  • 抽象方法必须被子类实现,则该类必须是一个抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
abstract class Shape {
// getArea方法只有声明没有实现体
// 实现让子类自己实现
// 可以将getArea方法定义为抽象方法: 在方法的前面加abstract
// 抽象方法必须出现在抽象类中, 类前面也需要加abstract
abstract getArea(): number;
}

class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}

getArea() {
return this.width * this.height;
}
}

class Circle extends Shape {
constructor(public radius: number) {
super();
}

getArea() {
return this.radius ** 2 * Math.PI;
}
}

class Triangle extends Shape {
constructor(public a: number, public b: number, public c: number) {
super();
}

getArea() {
let p = 0;
let s = 0;
if (this.a + this.b < this.c || this.a + this.c < this.b || this.b + this.c < this.a) {
throw new Error("不能构成三角形");
} else {
p = (this.a + this.b + this.c) / 2;
s = Math.sqrt(p * (p - this.a) * (p - this.b) * (p - this.c));
return s;
}
}
}

// 通用的函数
function calcArea(shape: Shape) {
return shape.getArea();
}

calcArea(new Rectangle(10, 20));
calcArea(new Circle(5));
calcArea(new Triangle(3, 4, 5));

// 在Java中会报错: 不允许
calcArea({
getArea(): number {
return 1;
}
});

// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc"

类的类型

类本身也是可以作为一种数据类型的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
name: string;
constructor(name: string) {
this.name = name;
}

running() {
console.log(this.name + " running!");
}
}

const p1 = new Person("aaa");
const p2: Person = {
name: "bbb",
running() {
console.log(this.name + " running!");
}
};

TS 类型检测-鸭子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TypeScript对于类型检测的时候使用的鸭子类型
// 鸭子类型: 如果一只鸟, 走起来像鸭子, 游起来像鸭子, 看起来像鸭子, 那么你可以认为它就是一只鸭子
// 鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型

class Person {
constructor(public name: string, public age: number) {}

running() {}
}

class Dog {
constructor(public name: string, public age: number) {}
running() {}
}

function printPerson(p: Person) {
console.log(p.name, p.age);
}

printPerson(new Person("why", 18));
// printPerson("abc")
printPerson({ name: "kobe", age: 30, running: function () {} });
printPerson(new Dog("旺财", 3));

const person: Person = new Dog("果汁", 5);

对象类型属性修饰符

对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息

  • 可选属性(Optional Properties)
    • 在属性名后面加一个 ? 标记表示这个属性是可选的
  • 只读属性(Readonly Properties)
    • 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为
    • 但在类型检查的时候,一个标记为 readonly的属性是不能被写入的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义对象类型
type IPerson = {
// 属性?: 可选的属性
name?: string;
// readonly: 只读的属性
readonly age: number;
};

interface IKun {
name?: string;
readonly slogan: string;
}

const p: IPerson = {
name: "why",
age: 18
};

// p.age = 30

索引签名

索引签名的含义

  • 有的时候,不能提前知道一个类型里的所有属性的名字,但是知道这些值的特征
  • 这种情况,就可以用一个索引签名 (index signature) 来描述可能的值的类型

索引签名的用法

  • 一个索引签名的属性类型必须是 string 或者是 number
  • 虽然 TypeScript 可以同时支持 stringnumber 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1.索引签名的理解
interface InfoType {
// 索引签名: 可以通过字符串索引, 去获取到一个值, 也是字符串
[key: string]: string;
}

function getInfo(): InfoType {
const abc: any = "haha";
return abc;
}

const info = getInfo();
console.log(info.name, info.age, info.address);

// 2.索引签名的案例
interface ICollection {
[index: number]: string;
length: number;
}

function printCollection(collection: ICollection) {
for (let i = 0; i < collection.length; i++) {
const item = collection[i];
console.log(item.length);
}
}

const array = ["abc", "cba", "nba"];
const tuple: [string, string] = ["aaa", "北京"];
printCollection(array);
printCollection(tuple);

类型问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface IIndexType {
// 返回值类型的目的是告知通过索引去获取到的值是什么类型
// [index: number]: string
// [index: string]: any
// [index: string]: string
}

// 索引签名: [index: number]: string 没有报错
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: any 没有报错
// 索引要求必须是字符串类型 names[0] => names["0"]
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: string 会报错
// 严格字面量赋值检测: ["abc", "cba", "nba"] => Array实例 => names[0], names.forEach
// const names: IIndexType = ["abc", "cba", "nba"]
// names["forEach"] => function
// names["map/filter"] => function

两个签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface IIndexType {
// 两个索引类型的写法
[index: number]: string;
[key: string]: any;

// 要求一: 数字类型索引的类型, 必须是字符串类型索引的类型的 子类型
// 结论: 数字类型必须是比字符串类型更加确定的类型(需要是字符串类型的子类型)
// 原因: 所有的数字类型都是会转成字符串类型去对象中获取内容

// [index: number]: string
// [key: string]: string | number

// 要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合string类型返回的属性
// [index: number]: string
// [key: string]: string | number

// aaa: string 符合要求
// bbb: boolean 错误的类型
}

const names: IIndexType = ["abc", "cba", "nba"];
const item1 = names[0];
const forEachFn = names["forEach"];

names["aaa"];

严格的字面量赋值检测

This PR implements stricter obiect literal assianment checks for the purpose of catching excess or misspelled properties.

The PR implements the suggestions in #3755. Specifically:

  • Every object literal is initially considered “fresh”
  • When a fresh object literal is assigned to a variable or passed for a parameter of a non-empty target type, it is an error for the object literal to specify properties that don’t exist in the target type.
  • Freshness disappears in a type assertion or when the type of an object literal is widened.

简单对上面的英文进行翻译解释

  • 每个对象字面量最初都被认为是“新鲜的(fresh)”
  • 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性是错误的
  • 当类型断言或对象字面量的类型扩大时,新鲜度会消失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface IPerson {
name: string;
age: number;
}

// 1.奇怪的现象一:
// 定义info, 类型是IPerson类型
const obj = {
name: "aaa",
age: 18,

// 多了一个height属性
height: 1.88
};
const info: IPerson = obj; // 没有报错

// 2.奇怪的现象二:
function printPerson(person: IPerson) {}
const kobe = { name: "kobe", age: 30, height: 1.98 };
printPerson(kobe); // 没有报错

// 解释现象
// 第一次创建的对象字面量, 称之为fresh(新鲜的)
// 对于新鲜的字面量, 会进行严格的类型检测. 必须完全满足类型的要求(不能有多余的属性)
const obj2 = {
name: "why",
age: 18,

height: 1.88
};

const p: IPerson = obj2;