前言

TypeScript是微软开发的基于JavaScript(JS)的一个扩展语言,包含JS所有内容,并增加了静态类型检查、接口、泛型等特性,适合大型项目开发。

为什么需要TypeScript?主要是因为JavaScript有如下困扰:

  • 本身是动态数据类型,导致数据类型不清晰
  • 无法判断是否有逻辑漏洞
  • 可以访问不存在的属性
  • 无法发现低级的拼写错误

TypeScript是JavaScript的一个超集,主要用来进行代码的静态类型检查(在运行或编译前就发现并提出错误),这在大型项目中会很有用。

Python的TypingHit的灵感或许来自这里

编译TS

手动编译

浏览器等运行环境无法直接运行TypeScript,需要先将Ts转化为Js

安装TypeScript

1
npm i typescript -g 

新建index.ts文件

1
2
3
let str: string = "hello world";

console.log(str);

在终端运行

1
tsc index.ts

tsc为TypeScript Compiler(ts编译器)的缩写

在文件夹会生成一个同名的index.js文件

1
2
var str = "hello world";
console.log(str);

自动编译

可以启用自动监听

1
2
tsc --init
tsc --watch [路径名/文件名]

tsc --init会自动生成一个tsconfig.json文件,文件中的选项都是ts转化js的配置项。tsc --watch的作用是监听所在的路径是否有ts文件,如有则自动编译为js

常用的为下面三个属性

  • “strict”: true 是否开启严格模式
  • “target”: “es2016” 使用那种规范进行转化 es2016是指es7 也可以直接写ES7
  • “noEmitOnError”: true 当ts文件有语法错误的是否是否编译
  • “module”: “ES6” 指定ts使用的模块化规范 如”commonjs”,”ES6”等

快速上手

ts的基本用法即为为变量添加类型申明。

编写如下代码:

1
2
3
4
5
6
7
8
9
10
let userName: string = "hello world";
let age: number = 20;
let isMan: boolean = true;

function add(a: number, b: number): number {
return a + b; // 返回类型要与规定的类型一致
}

let result: number = add(1, 2); // 此处参数不能多也不能少 如果只写一个,js会定义另一个为undefined
console.log(result);

编译得到的js如下

1
2
3
4
5
6
7
8
9
"use strict";
let userName = "hello world";
let age = 20;
let isMan = true;
function add(a, b) {
return a + b;
}
let result = add(1, 2);
console.log(result);

ts存在默认的类型推断

如:

1
2
let num = 0;
num = "hello" // Error

ts语法检查器默认将num指定为number类型,因此上述代码会出现“不能将类型“string”分配给类型“number”的报错

类型总览

支持的类型

JavaScript有如下数据类型

  • string
  • number
  • boolean
  • null
  • undefined
  • bigint
  • symbol
  • object (包含 Array Function Date Error等)

TypeScript有如下数据类型:

  • 支持所有JavaScript类型
  • 六个独有类型
    • any
    • unkown
    • never
    • void
    • tuple
    • enum
  • 两个用于自定义类型的方式
    • type
    • interface

大小写的区别

需要注意基本元素类型(小写)和包装器对象(大写)的区别:

1
2
3
4
5
6
7
8
9
10
11
let str1: string;
let str2: String;

str1 = "Hello";
// str1 = new String("World"); // Error - Type 'String' is not assignable to type 'string'

str2 = "Hello";
str2 = new String("World");

console.log(typeof str1); // string
console.log(typeof str2); // object

报错为不能将类型“String”分配给类型“string”。“string”是基元,但“String”是包装器对象。如可能首选使用“string”

报错显示String表示的是包装器对象,而string代表的是基元,因此出现了语法错误。ts官方推荐使用小写的类型申明

原始类型:它们内存占用少,处理速度快

包装对象:复杂类型,内存占用更多,在实际代码中很少使用

自动装箱:JavaScript在必要时会讲原始类型转化为包装对象,方便调用方法和属性(解释器自动完成的)

一个自动装箱的例子:

1
2
3
4
5
6
7
8
9
10
11
12
let str = "hello";

let size = (function () {
// 创建一个临时的字符串对象
let tempStringObject = new String(str);
// 访问字符串对象的 length 属性
let lengthValue = tempStringObject.length;
// 释放临时字符串对象 返回字符串长度
return lengthValue;
})();

console.log(size); // 5

常用类型

any

any表示当前的变量可以是任意的数据类型,一旦赋予any属性,就代表完全放弃了类型检查

1
2
3
4
5
6
7
8
9
// 显式any
let a: any;

a = "Hello";
a = 10;
a = true;

// 隐式any 悬停可观察
let b;

注意点:any类型可以覆盖任何数据类型,这会导致类型原本规定的类型改变

1
2
3
4
5
6
7
8
9
10
11
let a: any;

a = "Hello";
a = 10;
a = true;

let b: string;
b = a;

console.log(b); // true
console.log(typeof b); // boolean

另外,any类型可以调用任何属性与方法(即便本身类型不存在)

1
2
3
4
5
6
7
8
9
let a: any;

a = 9;

// 以下都不会报错
a.toUpperCase();
a.length();
a.eee();
a.ppp();

非迫不得已,不要使用any类型

unknown

unkown是一个类型更加安全的any,也表示当前数据类型可以是任意的值

1
2
3
4
5
6
7
8
let a: unknown;

a = "Hello";
a = 10;
a = true;

let b: string;
b = a; // 不能将类型“unknown”分配给类型“string”

从上面的实例可以看出unkown不会出现把原本变量类型覆盖的情况(直接报错)。但是也会因此出现类型相同但是无法赋值的情况:

1
2
3
4
5
6
7
8
9
let a: unknown;

a = 9;
a = true;
a = "hello";

console.log(a); // hello

let b: string = a; // 不能将类型“unknown”分配给类型“string”。

可以有三种方法解决这种问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a: unknown;
a = "hello";

let b: string;

// 第一种方法
if (typeof a === "string") {
b = a;
}

// 第二种方法 断言
b = a as string;

// 第三种方法 断言
b = <string>a;

unkown中,不能调用任何属性和方法

1
2
3
4
5
6
7
let a:unknown;

a = "hello";

a.toUpperCase(); // 报错 a是unknown类型
a.trim(); // 报错 a是unknown类型
a.length; // 报错 a是unknown类型

同理可以使用断言来告诉编译器变量的类型

1
2
3
4
5
6
let a: unknown;
a = "hello";

console.log(a); // hello
(<string>a).toUpperCase();
(a as string).toUpperCase();

never

never表示永远不会返回结果

不要将变量定义为never,这将导致对象无法被赋予任何值,而且这也没有意义

1
2
3
4
5
let a:never;

a = 99; // 报错 不能将number类型赋值给never类型
a = "hello"; // 报错 不能将string类型赋值给never类型
a = true; // 报错 不能将boolean类型赋值给never类型

never一般用于表示函数永远不会正常结束,如下三种例子:抛出异常、无限循环(无法结束)、自我无限调用

1
2
3
4
5
6
7
8
9
10
11
12
function demo(): never {
throw new Error("Error");
}

function demo2(): never {
while (true) {
}
}

function demo3(): never {
return demo3();
}

never一般是编译器自动推断出来的

1
2
3
4
5
6
7
let str :string = 'Hello'

if (typeof str === 'string') {
console.log(str.toUpperCase())
} else {
console.log(str); // let str: never
}

上述代码,else的部分永远无法到达,所以会被自动推断为never类型

void

void表示值为空,函数调用者不需要关心返回值,无法使用返回值做任何操作

常用于表示函数并没有返回值,且在形式上只接受undefined作为返回值

1
2
3
4
5
6
7
8
9
10
11
function demo() :void {
// 语法上隐式返回undefined
}

function demo2() :void {
return undefined;
}

function demo3() :void {
return;
}

另外调用者不能根据其返回值做任何操作

1
2
3
4
5
6
7
8
9
function demo(): void {
return undefined;
}

let result = demo();

if(result){ // Error:无法测试"void"类型表达式的真实性

}

这点和将返回值定义为undefined是有明显的不同的

1
2
3
4
5
6
7
8
9
function demo(): undefined {
return undefined;
}

let result: undefined = demo();
// 不会报错
if (result) {
console.log("result is not undefined");
}

如果将函数返回值定义为void,那么:

语法上:函数可以返回undefined,无论是显示返回,还是隐式返回

语义上:函数调用者无需关心函数的返回值,也不应依赖返回值进行任何操作

object

基本用法

object 表示一个对象,他可以是基元之外的任意数据类型。

Object 在object的基础上可以表示基元,它们都不能表示null或者undefined

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
let a: object;


a = { name: "Max" };
a = {};
a = function () {};
a = [];

// a = 1 // Error 不能将number赋值给object
// a = true // Error 不能将boolean赋值给object
// a = 'string' // Error 不能将string赋值给object
// a = null; // Error 不能将null赋值给object
// a = undefined; // Error 不能将undefined赋值给object


let b: Object;
b = { name: "Max" };
b = {};
b = function () {};
b = [];

b = 1;
b = true;
b = "string";

// b = null; // Error 不能将null赋值给Object
// b = undefined; // Error 不能将undefined赋值给Object

从上述代码可以看出,object表示的范围过于宽泛,因此在实际中并不使用

替代用法

开发中object并不常用,那么我们如何定义函数、对象、数组的类型呢?

  1. 函数

    基础的用法如下所示

    1
    2
    3
    4
    5
    6
    7
    let add: (a: number, b: number) => number;

    add = function (a, b) {
    return a + b;
    };

    add = (a, b) => a + b;

    另外还可以使用接口,自定义类型等方式进行定义

  2. 对象

    基础的用法如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let person: {
    name: string;
    age: number;
    [key: string]: any; // 表示可以有任意数量的其他属性,但是key必须是string类型
    };

    person = {
    name: "gcnanmu",
    age: 30,
    gender: "man",
    };

    其中的key其实只是个标识符,换什么都可以

  3. 数组

    基础用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arr: string[];

    let arr2: Array<string>;

    arr = ["a", "b", "c"];
    // arr = [1, 2, 3]; // Error: Type 'number' is not assignable to type 'string'.

    arr2 = ["a", "b", "c"];
    // arr2 = [1, 2, 3]; // Error: Type 'number' is not assignable to type 'string'.

    Array<string>属于泛型

tuple

tuple类型并非一个关键字,它只是一种形式,可以理解为一种特殊的数组类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict";
let tuple;
let tuple2;
let tuple3; // 表示可以除了第一个元素外,还可以有任意多个string类型的元素
tuple = [1, "hello"];
// tuple = [1, 'hello', 2]; // Error 不能将类型“[number, string, number]”分配给类型“[number, string]”
// tuple = ['hello', 1]; // Error 不能将类型“[string, number]”分配给类型“[number, string]”
tuple2 = [1, "hello"];
tuple2 = [1];
// tuple2 = ["hello"]; // Error 不能将类型“[string]”分配给类型“[number, string?]”
tuple3 = [1, "hello"];
tuple3 = [1, "hello", "world"];
tuple3 = [1];
tuple3 = [1, "hello", "world", "foo"];
// tuple3 = ["hello"]; // Error 不能将类型“[string]”分配给类型“[number, ...string[]]”

console.log(typeof tuple, typeof tuple2, typeof tuple3); // object object object

enum

困境

enum表示枚举类型,用于定义一组命名的常量,能增强代码的可读性,让代码更好维护。

常见的困境如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getDirection(direction: string): string {
switch (direction) {
case "Up":
return "向上";
case "Down":
return "向下";
case "Left":
return "向左";
case "Right":
return "向右";
default:
return "其他方向";
}
}

以上代码虽然逻辑上是没有问题的,但是无法防止出现写错的隐患,且代码的可读性也比较差。可以通过枚举类型将所有方向信息装入一个类型中。

数字枚举

1
2
3
4
5
6
7
8
enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction);

打印得到的结果为:

1
2
3
4
5
6
7
8
9
10
{
'0': 'Up',
'1': 'Down',
'2': 'Left',
'3': 'Right',
Up: 0,
Down: 1,
Left: 2,
Right: 3
}

从上述输出可以看出Direction是一个对象,且标号从0开始递增,且具有反身性。

定义好Direction后,我们就可以修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Direction {
Up,
Down,
Left,
Right,
}

function getDirection(direction: Direction): string {
switch (direction) {
case Direction.Up:
return "向上";
case Direction.Down:
return "向下";
case Direction.Left:
return "向左";
case Direction.Right:
return "向右";
default:
return "其他方向";
}
}

这样改写后的好处在于代码的可读性变高,且写代码的时候会出现选择项(只能从定义的类型中选择,无其他选项),可以杜绝写错的情况。

如果要更改递增的开始数字,可以修改第一个变量的赋值。

1
2
3
4
5
6
7
8
enum Direction {
Up = 10,
Down,
Left,
Right,
}

console.log(Direction)

得到的打印结果为

1
2
3
4
5
6
7
8
9
10
{
'10': 'Up',
'11': 'Down',
'12': 'Left',
'13': 'Right',
Up: 10,
Down: 11,
Left: 12,
Right: 13
}

实际上,我们无需关心它实际代表的取值

字符串枚举

enum中的变量值可以不用number,使用string

1
2
3
4
5
6
7
8
enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}

console.log(Direction)

打印得到的结果如下

1
2
3
4
5
6
enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}

从输出可以看出,这样做之后就不再具有反身性

常量枚举

如果基本的用法来说,编译得到的js可能会出现很多冗余的代码

1
2
3
4
5
6
7
8
enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}

console.log(Direction.Up);

得到的js如下

1
2
3
4
5
6
7
8
9
"use strict";
var Direction;
(function (Direction) {
Direction["Up"] = "Up";
Direction["Down"] = "Down";
Direction["Left"] = "Left";
Direction["Right"] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);

在js中我们得到了整个枚举类型的转意,但是实际上,我们只使用了Direction.Up这一个属性,那么其他信息就是冗余的。

改进的方法就是在enum前面添加一个const关键字

1
2
3
4
5
6
7
8
const enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}

console.log(Direction.Up);

编译得到的js就会删除多余的冗余部分(会留下提示),显著降低代码量

1
2
"use strict";
console.log("Up" /* Direction.Up */);

官方的描述:常量枚举是一种特殊的枚举类型,他使用const关键字定义,在编译时会被内联,避免生成一些额外的代码

何为编译时的内联?

TypeScript在编译时,会讲枚举成员引用替换为它们的实际值,而不是生成额外的枚举对象。这样可以减少生成的JavaScript代码量,提高运行时候的性能。

type

type的作用是用于给类型起别名,这可以使编写代码更加的高效,增强代码可读性

1
2
3
4
// type的作用是给类型起别名
type shuzi = number;

let num: shuzi = 123;

内联类型

将多个可能的类型合并为一个新类型,这个类型可以是多个类型中的任意一个(类型与类型之间是或的关系)

1
2
3
4
5
6
7
8
9
10
11
12
13
type shuzi = number | string;
type zifu = string | boolean;
type gender = "男" | "女" | "保密";

let shuzi1: shuzi = 1;
let shuzi2: shuzi = "1";

let zifu1: zifu = "1";
let zifu2: zifu = false;

let gender1: gender = "保密";
let gender2: gender = "男";
let gender3: gender = "女";

上述代码gender类型填写具体值时会出现提示选择。

交叉类型

将多个可能的类型合并为一个新类型,这个类型必须满足所有类型的要求(类型与类型之间是且的关系)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person = {
name: string;
age: number;
};

type Address = {
city: string;
country: string;
};

type demo = Person & Address;

// 需要满足Perosn和Address所有的要求
const info: demo = {
name: "John",
age: 30,
city: "New York",
country: "USA",
};

如果交叉类型的类型之间不存在交集,那么ts会自动推断为never

1
type demo = string & number

一个特殊的情况

在函数定义的时候,我们就规定了函数的返回值为void

1
2
3
4
5
6
7
8
function addNumber(a: number, b: number): void {
console.log(a + b);
return undefined; // OK
// return 999; // Error: 不能将类型"number"分配给类型"void"
// return null; // Error: 不能将类型"null"分配给类型"void"
// return [] // Error: 不能将类型"never[]"分配给类型"void"
// return {} // Error: 不能将类型"{}"分配给类型"void"
}

这个时候,按照在上文中介绍的void的使用规则没有发生任何变化,它还是只能接受undefined作为返回值。

但是如果我们采用在object中的方法,先定义函数的返回值类型为void,那么就会出现如下的反常现象。

1
2
3
4
5
6
7
8
9
10
11
let add: (a: number, b: number) => void;

add = (a, b) => 999;

add = function (a, b) {
return 999;
};

add = (a, b) => {
return 999;
};

从上述代码可以看到,即便是规定了函数返回值为void,但是还是能够返回除了undefined的其他任意值。

按照官方文档的描述,这是为了兼容如下的情况

1
2
3
4
const src = [1, 2, 3];
const dst = [0];

src.forEach((el) => dst.push(el));

forEach的文档为

(method) Array*<number*>.forEach(callbackfn: (value*:* number, index*:* number, array*:* number[]) => void, thisArg*?:* any): void

可以看到callbackfn的返回值为void,但是(el) => dst.push(el)方法得到的返回值是dst的长度,这样会出现不兼容的情况,这算是一个妥协。

但是返回值还是符合void值无法被使用的情况。

1
2
3
4
5
6
7
8
9
10
11
let add: (a: number, b: number) => void;

add = (a, b) => 999;

let result = add(10, 20);

if(result){ // Error,无法测试"void"类型表达值的真实性

}

let resultPlus = result + 100; // Error: 运算符"+"不能应用于类型"void"和"number"。

复习类的知识

在ts中,类的定义和js没有明显的不同,比较明显的不同在使用constructor的时候,需要先在类的开头申明。重写父类方法时,可以使用override来获取重写的提示。

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;
}

say(): void {
console.log(`I am ${this.name}, I am ${this.age} years old.`);
}
}

const p = new Person("Tom", 18);
console.log(p);
p.say();

class Student extends Person {
grade: string;
constructor(name: string, age: number, grade: string) {
// 将参数传递给父类
super(name, age);
this.grade = grade;
}
study(): void {
console.log(`I am ${this.name}, I am studying.`);
}

// 重构父类方法
override say(): void {
console.log(
`I am ${this.name}, I am ${this.age} years old, I am a student.`
);
}
}

const stu = new Student("Jerry", 20, "大三");
console.log(stu);
stu.say();

在ts中,constructor有一个简写方法,这在后文的属性修饰符中会讲到

属性修饰符

属性修饰符是用来限定类中属性与方法的权限,写在类属性或者方法之前,例如public name: string;

修饰符 含义 具体规则
public 公开的 可以被:内部类、子类、类外部访问
protected 受保护的 可以被:类内部、子类访问
private 私有的 可以被:类内部访问
readonly 只读属性 属性无法修改

public

顾名思义,即表示属性是公开的,如果不限定属性的修饰符,那么ts默认认定属性被public所修饰

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
class Person {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

public say(): void {
console.log(`I am ${this.name}, I am ${this.age} years old.`);
}
}

class Student extends Person {
public grade: string;
constructor(name: string, age: number, grade: string) {
super(name, age);
this.grade = grade;
}

study() {
console.log("I am a student. I am studying.");
}
}

const p = new Student("Tom", 18);
// 都不会报错
p.name;
p.age;
p.grade;
p.say();
p.study();

如果属性有属性写了修饰符,那么不建议省略public

属性的简写形式

在ts中,可以使用如下的形式简写constructor函数

1
2
3
class 类名 {
constructor(修饰符 A: 类型, 修饰符 B: 类型)
}

将上文中类的代码简写为如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
constructor(public name: string, public age: number) {}

public say(): void {
console.log(`I am ${this.name}, I am ${this.age} years old.`);
}
}

class Student extends Person {
// 简写形式
constructor(
public name: string,
public age: number,
public grader: string
) {
super(name, age);
}

study() {
console.log("I am a student. I am studying.");
}
}

需要注意的是,子类中,如果需要新传入属性,那么在构造函数constructorsuper()是无法省略的。

protected

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
class Person {
constructor(protected name: string, protected age: number) {}

protected getDetails(): string {
return `Name: ${this.name}, Age: ${this.age}`;
}

introduce(): void {
console.log(this.getDetails());
}
}

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

study(): void {
// 可以调用父类受到protected修饰的方法
this.introduce();
// 可以调用父类受到protected修饰的属性
console.log(`${this.name} is studying`);
}
}

const p1 = new Person("Alice", 30);
p1.introduce(); // Name: Alice, Age: 30
p1.name; // 属性name受保护,只能在类“Person”及其子类中访问
p1.age; // 属性age受保护,只能在类“Person”及其子类中访问
p1.getDetails(); // 方法“getDetails”受保护,只能在类“Person”及其子类中访问

const s1 = new Student("Bob", 20, "A");
s1.study(); // Name: Bob, Age: 20 Bob is studying
s1.name; // 属性name受保护,只能在类“Person”及其子类中访问
s1.age; // 属性age受保护,只能在类“Person”及其子类中访问
s1.grade; // 属性grade受保护,只能在类“Student”及其子类中访问

上述代码中,agename是收保护的属性,它能够被类内部和继承类Student调用,但是无法被外部调用。在Student类中调用了introduce方法,introduce又实现了收保护的getDetails方法,这说明继承类能够调用父类中受到保护的protected方法。它们都不能被外部调用。

private

意味私有的,只能在当前类中被访问,不能被子类和外部访问。

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
class Person {
constructor(
public name: string,
public age: number,
private idCard: string
) {}

private getPrivateInfo(): string {
return `My ID card is ${this.idCard}`;
}

getInfo(): string {
return `My name is ${this.name}, I'm ${this.age} years old`;
}

getFullInfo(): string {
return `${this.getInfo()}, ${this.getPrivateInfo()}`;
}
}

class Student extends Person {
getStudentInfo(): string {
this.idCard; // 属性“idCard”为私有属性,只能在类“Person”中访问
this.getPrivateInfo(); // 属性“getPrivateInfo”为私有属性,只能在类“Person”中访问
return this.getFullInfo();
}
}

const p1 = new Person("Alice", 20, "123456789");
console.log(p1.getInfo()); // My name is Alice, I'm 20 years old
console.log(p1.getFullInfo()); // My name is Alice, I'm 20 years old, My ID card is 123456789
console.log(p1.idCard); // 属性“idCard”为私有属性,只能在类“Person”中访问。
console.log(p1.getPrivateInfo()); // console.log(p1.idCard); // 属性“idCard”为私有属性,只能在类“Person”中访问。

上述代码中,私有方法getPrivateInfo可以被类中的getFullInfo调用,但是无法被继承的Student和外部调用。私有属性idCard可以被类中的私有方法getPrivateInfo调用,但是不能被继承的Student和外部调用。

实际上,在外部调用getFullInfo间接调用了getPrivateInfo方法

readonly

顾名思义,即只可读的,不可被修改,写在属性修饰符之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
constructor(
public name: string,
public readonly age: number
) {}

getInfo(): string {
return `${this.name} is ${this.age} years old`;
}
}

const person = new Person('John', 30);
person.getInfo(); // John is 30 years old

person.name = 'Tom'; // OK
person.getInfo(); // Tom is 30 years old
person.age = 31; // 无法为“age”赋值,因为它是只读属性。

上述代码中,Person的age属性为只读属性,因此无法被修改。

抽象类

概述:抽象类是一种无法被实例化的类,专门用来定义类的结构和行为,类中可以写抽象方法,也可以具体实现,拙象类主要用来为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法。

简记:抽象类不能实例化,其意义是可以被继承,抽象类里可以有普通方法,也可以有抽象方法

限定一个抽象的package对象

1
2
3
4
5
6
7
8
9
abstract class Package {
constructor(public weight: number) {}

abstract calculate(): number;

printPackage() {
console.log(`Weight: ${this.weight} Price: ${this.calculate()}`);
}
}

在抽象对象中,规定了package的calculate的计算方法,这个方法必须被继承的子类实现。因为printPackage调用了calculate这个抽象方法,但是抽象方法却没有具体的实现方式,如果成功调用会出现严重的问题,这个也可以解释抽象类是不能被实例化的。

根据快递种类的不同,定义StandardPackageExpressPackage两个类

1
2
3
4
5
6
7
8
9
class StandardPackage extends Package {
constructor(weight: number, public unitPrice: number) {
super(weight);
}

calculate(): number {
return this.weight * this.unitPrice;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ExpressPackage extends Package {
constructor(
weight: number,
public unitPrice: number,
public additional: number
) {
super(weight);
}

calculate(): number {
if (this.weight <= 10) {
return this.weight * this.unitPrice;
} else {
return this.unitPrice * 10 + (this.weight - 10) * this.additional;
}
}
}

可以看到两种快递的计算方式不同,但是都需要计算费用这个方法。因此从本质上来说,abstract类用来规定子类必须有相同的方法,但是可以有不同的实现方式(可以解释为什么abstract不能有具体的实现方式)

1
2
3
4
5
const p1 = new StandardPackage(13, 8);
p1.printPackage(); // Weight: 13 Price: 104

const p2 = new ExpressPackage(13, 8, 2);
p2.printPackage(); // Weight: 13 Price: 86

何时可以使用抽象类

  1. 定义通用的接口:为一组相关的类定义通用的行为(方法或属性)时
  2. 提供基础的实现:在抽象类中提供某些方法或者为其提供基础实现,这样派生类就可以继承这些实现
  3. 确保关键实现:强制派生类实现一些关键行为
  4. 共享代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复

interface(接口)

interface是一种定义结构的方式,主要的作用是为类,对象、函数等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意interface只能定义格式,不能包含任何具体实现。

定义类的结构

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
interface PersonInterface {
name: string;
age: number;
sayHello(n: number): void;
}

class Person implements PersonInterface {
// 必须定义name,age
constructor(
public name: string,
public age: number,
// 可以有自己额外的属性
public grade: string
) {}

// 必须实现sayHello方法
sayHello(n: number): void {
console.log(`Hello ${this.name} ${n}`);
}

// 可以有自己额外的方法
showMyGrade(): void {
console.log(`My grade is ${this.grade}`);
}
}

const p = new Person("John", 30, "A");
p.sayHello(10); // Hello John 10
p.showMyGrade(); // My grade is A

接口PersonInterface定义了实现类的一些约束,实现类必须实现这些约束,在此基础上,也可以有自己的方法。

接口有两种常用的命名规范:

  1. 在接口名前添加I,如IPerson
  2. 在接口名后添加Interface,如PersonInterface

定义对象的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface PersonInterface {
name: string;
age: number;
run(n: number): void;
}

const person: PersonInterface = {
name: "John",
age: 20,
run: (n: number) => {
console.log(`${person.name} is running ${n} miles`);
},
// 对象字面量只能指定已知属性,并且“sayHi”不在类型“PersonInterface”中
// sayHi ():void {
// console.log('Hi');
// }
};
person.run(5); // John is running 5 miles

对象在继承接口的时候不需要使用implementsperson对象不能出现PersonInterface规定之外的其他属性和方法。这种接口的作用和type的作用几乎没有任何区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type PersonInterface = {
name: string;
age: number;
run(n: number): void;
}

const person: PersonInterface = {
name: "John",
age: 20,
run: (n: number) => {
console.log(`${person.name} is running ${n} miles`);
},
// 对象字面量只能指定已知属性,并且“sayHi”不在类型“PersonInterface”中
// sayHi ():void {
// console.log('Hi');
// }
};
person.run(5); // John is running 5 miles

定义函数的结构

1
2
3
4
5
6
7
interface AddInterface {
(a: number, b: number): number;
}

const add: AddInterface = (a, b) => {
return a + b;
};

这个和之前的函数申明有异曲同工之妙。

1
2
3
let AddType: (a: number, b: number) => number;

AddType = (a, b) => a + b;

接口之间继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface PersonInterface {
name: string;
age?: number;
}

interface Student extends PersonInterface {
grader: string
}

// 继承后所有接口规定的都要实现
const student :Student = {
name: 'John Doe',
// 可选属性
// age: 20,
grader: 'A'
}

接口之间可以互相继承,继承后的接口会将父类的限制添加到自身。简而言之,实现的子类要实现所有之前所有父类的定义属性和方法。

接口的自动合并(可重复定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Gender {
man = "男",
woman = "女",
}

interface PersonInterface {
name: string;
age: number;
}

interface PersonInterface {
gender: Gender;
}

const person: PersonInterface = {
// 需要完成PersonInterface的所有属性
name: "Tom",
age: 24,
gender: Gender.man,
};

console.log(person);

当接口出现命名重复的时候,接口的实现并不会相互覆盖,而是出现自动合并。可以理解为同名接口的继承(但实际上没有这种语法,ts编译器会出现报错类型“PersonInterface”以递归方式将自身引用为基类。)。

总结:何时使用接口

  1. 定义对象的格式:描述数据模型、API响应格式、配置对象等等,是开发中用的最多的场景
  2. 类的契约:规定一个类需要实现哪些属性和方法
  3. 自动合并:一般用于扩展第三方库的类型,这种特性在大型项目中可能会用到

一些相似概念的区别

接口与type

接口interfacetype都可以限制对象结构,在使用方面是可以相互替代的。两者都有一些自己的特点。

  1. interface:更加专注于定义对象和类的结构,支持继承和合并
  2. type:可以定义类型别名、联合类型、交叉类型,但不支持继承和自动合并
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
type PersonType = {
name: string;
age: number;
run(n: number): void;
};

interface PersonInterface {
name: string;
age: number;
run(n: number): void;
}

const p1: PersonInterface = {
name: "John",
age: 20,
run: (n: number) => {
console.log(p1.name + " Running " + n + " miles");
},
};

const p2: PersonType = {
name: "Tom",
age: 16,
run: (n: number) => {
console.log(p2.name + " Running " + n + " miles");
},
};

p1.run(10); // John Running 10 miles
p2.run(20); // Tom Running 20 miles

观察上述代码可以看出,PersonTypePersonInterface的作用是一样的,都限制了对象的结构,甚至实现的结构也完全相同,两者可以相互对调。

interface可以进行继承和自动合并,这是type不具备的

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

interface PersonInterface2 extends PersonInterface {
gender: string;
}

const p1: PersonInterface2 = {
name: "John",
age: 20,
gender:"man",
};

上述写法可以等价于下面的写法(自动合并)

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

interface PersonInterface {
gender: string;
}

const p1: PersonInterface = {
name: "John",
age: 20,
gender: "man",
};

另一方面,type虽然不能使用继承,但可以通过交叉类型来实现上述的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type PersonInterface  = {
name: string;
age: number;
}

type PersonType = PersonInterface & {
gender: string;
}

const p1: PersonType = {
name: "John",
age: 20,
gender: "man",
};

实际上,interfacetype写法更简洁

接口与抽象类

  • 相同点:接口和抽象类都可以定义一个类的格式
  • 不同点:
    1. 接口:只能描述结构,不能有任何代码实现,一个类可以实现多个接口
    2. 抽象类:既可以包含抽象方法,也可以含有具体实现,一个类只能继承一个抽象类

一个类可以一次性继承多个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface FlyInterface {
fly(): void;
}

interface SwimInterface {
swim(): void;
}

class Duck implements FlyInterface, SwimInterface {
fly() {
console.log("Duck is flying");
}

swim() {
console.log("Duck is swimming");
}
}

const duck = new Duck();
duck.fly(); // Duck is flying
duck.swim(); // Duck is swimming
1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Duck {
abstract fly(): void;
abstract swim(): void;
}

class MallardDuck extends Duck {
fly() {
console.log("MallardDuck is flying");
}

swim() {
console.log("MallardDuck is swimming");
}
}

接口只动口不动手,抽象类是可以自己动手的。

泛型

泛型允许我们在定义函数、类或接口时,使用类型参数来表示未指定类型,这些参数在具体使用时,才被置顶具体的类型。这能让同一代码使用多数据类型,同时保持类型的安全性。

泛型函数

当我们暂时无法确定要传入的参数的类型时,可以使用泛型告知编译器参数可以是任意的类型。

1
2
3
4
5
6
7
8
function printData<T>(data1: T): void {
console.log(data1);
}

// 先告知参数的类型
printData<string>("Hello World"); // Hello World
printData<number>(123); // 123
printData<boolean>(true); // true

当时当使用者调用的时候,需要提前告知其传入的参数类型。

实际上<T>中的T只是一个标识符,可以替换为任意的字符

多个泛型

泛型可以不只有一个

1
2
3
4
5
6
7
function printData<T, U>(data1: T, data2: U): void {
console.log(data1, data2);
}

// 先告知参数的类型
printData<string, number>("Hello World", 222); // Hello World 222
printData<number, boolean>(123, false); // 123 false

返回值也可以设置为泛型

1
2
3
4
5
function printData<T, U>(data1: T, data2: U): T | U {
return Math.random() % 2 === 0 ? data1 : data2;
}

console.log(printData<string, number>('Hello', 123));

泛型接口

在接口中也可以使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface PersonInterface<T> {
name: string;
age: number;
moreInfo: T;
}

type PE = {
height: number;
weight: number;
};

const person: PersonInterface<PE> = {
name: "John",
age: 30,
moreInfo: {
height: 180,
weight: 80,
},
};

泛型约束

可以使用继承的方式限定泛型的取值范围

1
2
3
4
5
6
7
8
9
10
interface PersonInterface {
name: string;
age: number;
}

function logPerson<T extends PersonInterface>(info: T): void {
console.log(`My name is ${info.name} and I am ${info.age} years old.`);
}

logPerson({ name: "John", age: 30 });

泛型类

泛型可以在类中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Student<T> {
constructor(public name: string, public age: number, public moreInfo: T) {}

speak() {
console.log(`My name is ${this.name}, I am ${this.age} years old`);
console.log(this.moreInfo);
}
}

type jobInfo = {
job: string;
salary: number;
};

const student = new Student<jobInfo>("Tom", 20, {
job: "developer",
salary: 1000,
});

student.speak()
// My name is Tom, I am 20 years old
// { job: 'developer', salary: 1000 }

类型申明文件

在npm官网,还有很多的库是使用js编写的,不能直接在ts中导入。

新建一个demo.js文件,导出相应的模块

1
2
3
4
5
6
7
8
// demo.js
export function add(a, b) {
return a + b;
}

export function abstract(a, b) {
return a - b;
}

这时候如果直接在index.ts中导入,就会出现无法找到模块“./demo.js”的声明文件。“d:/Web/ts/demo.js”隐式拥有 "any" 类型。的报错,这是因为js不支持编写类型提示。

1
2
3
4
5
6
// index.ts
import { add, abstract } from "./demo.js";
// 无法找到模块“./demo.js”的声明文件。“d:/Web/ts/demo.js”隐式拥有 "any" 类型。

console.log(add(1, 2));
console.log(abstract(1, 2));

此时需要在文件中新建一个同名的demo.d.ts文件,加入如下代码

1
2
3
4
5
declare function add(a: number, b: number): number;

declare function abstract(a: number, b: number): number;

export { add, abstract };

declare意味申明,这样可以告知ts编译器同名的demo.js中数据的具体类型。编写后需要重启vscode,之后就可以在index.ts中悬停得到相应的代码提示。

注:

  1. 在项目,这些文件通常放在@types文件夹中
  2. 对于一些老的js库,可以在npm按照@types/库名的方式直接搜到相应的.d.ts文件。如@types/jquery