TypeScript 学习

一、TypeScript 介绍

什么是 TypeScript?

  • TypeScript 简称 TS
  • TS 和 JS 之间的关系其实就是 Less/Sass 和 CSS 之间的关系
  • 就像 Less/Sass 是对 CSS 进行扩展一样, TS 也是对 JS 进行扩展
  • 就像 Less/Sass 最终会转换成 CSS 一样, 我们编写好的 TS 代码最终也会换成 JS
  • TypeScript 是 JavaScript 的超集,因为它扩展了 JavaScript,有 JavaScript 没有的东西。
  • 硬要以父子类关系来说的话,TypeScript 是 JavaScript 子类,继承的基础上去扩展。

为什么需要 TypeScript?

  • 简单来说就是因为 JavaScript 是弱类型, 很多错误只有在运行时才会被发现
  • 而 TypeScript 提供了一套静态检测机制, 可以帮助我们在编译时就发现错误

TypeScript 的特点?

  • 支持最新的 JavaScript 新特特性
  • 支持代码静态检查
  • 支持诸如 C,C++,Java,Go 等后端语言中的特性 (枚举、泛型、类型转换、命名空间、声明文件、类、接口等)

二、TypeScript 的安装

安装命令

npm install typescript -g

查看版本

tsc --version

三、执行 TypeScript 两种方式

方式一:通过 ts-node 库

通过 ts-node 库,为 TypeScript 运行提供执行环境

TS 的执行环境

npm install ts-node -g

ts-node 是一个执行环境,把 TS 变成 JS 然后执行

ts-node 需要依赖 tslib 和 @types/node 两个包

npm install tslib @types/node -g

方式二:通过 Webpack 的本地服务

通过 webpack,配置本地的 TypeScript 编译环境和开启一个本地服务,可以直接运行在浏览器上;

1.生成 package.json 文件

$ npm init -y

2.安装 wepack 和 webpack-cli

$npm install webpack webpack-cli -D 局部安装

3.创建 webpack.config.js

4.安装 ts-loader 和 typescript

$npm install ts-loader typescript -D

5.安装 devServer

$npm install webpack-dev-server -D

6.生成 tsconfig 配置文件

$tsc -init

7.安装 HtmlWebpackPlugin 插件

该插件会生成一个 HTML5 文件

$npm install html-webpack-plugin -D

8.配置 WebpackConfig 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/main.ts",
output: {
path: path.resolve(__dirname, "build"),
filename: "bundle.js",
},
devServer: {},
resolve: {
extensions: [".ts", "..."], // ...访问默认扩展名
},
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
},
],
},
plugins: [new HtmlWebpackPlugin()],
};

9.在 package.json 文件里添加项目打包命令和运行命令

qG0gMD.png

10.打包项目和运行项目

打包 $npm run build

运行 $npm run serve

项目结构

qGBKSK.png

四、变量的定义格式

变量声明

  • 在 TypeScript 中定义变量需要指定 标识符 的类型。
  • 所以完整的声明格式如下:
    • 声明了类型后 TypeScript 就会进行类型检测,声明的类型可以称之为类型注解
    • var/let/const 标识符: 数据类型 = 赋值;
  • 比如我们声明一个 message,完整的写法如下:
    • 注意:这里的 string 是小写的,和 String 是有区别的
    • string 是 TypeScript 中定义的字符串类型,String 是 ECMAScript 中定义的一个类
    • let message:string = 'Hello World';
  • 如果我们给 message 赋值其他类型的值,那么就会报错:

声明变量的关键字

  • 在 TypeScript 定义变量(标识符)和 ES6 之后一致,可以使用 var、let、const 来定义。
    qGBxne.png
  • 当然,在 tslint 中并不推荐使用 var 来声明变量:
    • 可见,在 TypeScript 中并不建议再使用 var 关键字了,主要原因和 ES6 升级后 let 和 var 的区别是一样的,var 是没 有块级作用域的,会引起很多的问题。

变量的推导(推断)

在开发中,有时候为了方便起见我们并不会在声明每一个变量时都写上对应的数据类型,我们更希望可以通过 TypeScript 本身的 特性帮助我们推断出对应的变量类型:

qGDCtI.png

如果给 foo 重新赋值 123

qGDPht.png

  • 这是因为在一个变量第一次赋值时,会根据后面的赋值内容的类型,来推断出变量的类型:
    • 上面的 foo 就是因为后面赋值的是一个 string 类型,所以 foo 虽然没有明确的说明,但是依然是一个 string 类型;

五、JavaScript 和 TypeScript 的数据类型

我们经常说 TypeScript 是 JavaScript 的超集

qGDtHJ.png

number 类型

数字类型是我们开发中经常使用的类型,TypeScript 和 JavaScript 一样,不区分整数类型(int)和浮点型 (double),统一为 number 类型。

1
2
3
4
5
6
7
8
let num1: number = 66;
let num2: number = 12.1;

// TypeScript 也支持二进制,八进制,十六进制的表示方式
let num3 = 100; //十进制
let num4 = 0b1001; // 二进制
let num5 = 0o147; // 八进制
let num6 = 0x18af; // 十六进制

boolean 类型

boolean 类型只有两个取值:true 和 false,非常简单

1
2
3
let flag: boolean = true;
flag = false;
flag = 60 < 30;

string 类型

string 类型是字符串类型,可以使用单引号或双引号

1
2
3
4
5
6
7
let message: string = "Hello World";

// 同样也支持ES6的模板字符串来拼接变量和字符串
const name = "小明";
const age = 18;
const height = 1.88;
console.log(`my name is ${name} age is ${age} height is ${height}`);

Array 类型

数组的定义方式有两种

1
2
const info: string[] = ["James", "kobe", "ldh"];
const info2: Array<string> = ["James", "kobe", "ldh"];

object 类型

1
2
3
4
5
6
7
const info = {
name: "why",
age: 18,
};
// 如果手动给对象添加类型注解的话会获取不到里面的值
// 推荐让TypeScript自动推导
console.log(info.name);

Symbol 类型

1
2
3
4
5
6
7
8
const title1 = Symbol("title");
const title2 = Symbol("title");

const info = {
[title1]: "程序员",
[title2]: "律师",
};
console.log(info[title1]);

null 和 undefined 类型

在 JavaScript 中,undefined 和 null 是两个基本数据类型。 n 在 TypeScript 中,它们各自的类型也是 undefined 和 null,也就意味着它们既是实际的值,也是自己的类型:

1
2
let n1: null = null;
let n2: undefined = undefined;

any 类型

  • 在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用 any 类型。
  • any 类型有点像一种讨巧的 TypeScript 手段:
    • 我们可以对 any 类型的变量进行任何的操作,包括获取不存在的属性、方法;
    • 我们给一个 any 类型的变量赋值任何的值,比如数字、字符串的值;
1
2
3
4
5
6
7
8
// 在 TypeScript 中,任何类型都归为any类型,这让 any 类型成为了类型系统的顶级类型。
// 如果是一个普通的类型,在赋值过程中改变类型是不被允许的,但如果是 any 类型,则允许赋值为任意类型
// 在 any 上访问任何属性都是允许的,也允许调用任何方法.
// 在不想给某些JavaScript 添加具体的数据类型时可以使用 any 类型(原生JavaScript代码是一样)
let message: any = "Hello World";
message = 123;
message = true;
message = {};

unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// unknown类型只能赋值给 any 和 unknown类型
// any类型可以赋值给任意类型

function foo(): string {
return "Hello World";
}

function bar(): number {
return 123;
}

let flag = true;
let result: unknown;
if (flag) {
result = foo();
} else {
result = bar();
}
let res: unknown = result; // ok
let res2: any = result; //ok
// let message: string = result; //Error
// let num: number = result; //Error

void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。
当一个函数没有返回值时,你通常会见到其返回值类型是 void:

1
2
3
4
function sum(num1: number, num2: number): void {
console.log(num1 + num2);
}
sum(30, 50);

never 类型

never 表示永远不会发生值的类型,比如一个函数:
如果一个函数中是一个死循环或者抛出一个异常,那么这个函数会返回东西吗?
不会,那么写 void 类型或者其他类型作为返回值类型都不合适,我们就可以使用 never 类型;

下面一个例子让我们认识 never 类型的应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleMessage(message: string | number) {
switch (typeof message) {
case "string":
console.log("string处理方式处理message");
break;
case "number":
console.log("number处理方式处理message");
break;
default:
const check: never = message;
}
}
handleMessage(123);

注意在 case 分支里面,我们把收窄为 never 的 message 赋值给一个显示声明的 never 变量。
如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 message 的类型:
然而他忘记同时修改 handleMessage 方法中的控制流程,
这时候 default 分支的 message 类型会被收窄为 boolean 类型,
导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,
我们可以确保 handleMessage 方法总是穷尽了 Foo 的所有可能类型。
通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,
目的就是写出类型绝对安全的代码。

tuble 类型

tuble 是元组类型,数组和元组有什么区别呢?
数组中通常建议存放相同类型的元素,不同类型的元素是不推荐存放在数组的(可以存放在对象或者元组中)
其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型

1
2
3
4
let arr: [string, number, number] = ["giao", 18, 1.88];

const name = arr[0];
console.log(name);

元组的应用场景

那么 tuple 在什么地方使用的是最多的呢?
tuple 通常可以作为返回的值,在使用的时候会非常的方便;

1
2
3
4
5
6
7
8
9
10
function useState<T>(state: T): [T, (newState: T) => void] {
let currentState = state;
const changeState = (newState: T) => {
currentState = newState;
};
return [currentState, changeState];
}

const [counter, setcounter] = useState(666);
setcounter(666);

六、 TypeScript 的类型补充

函数的参数和返回值类型

1
2
3
4
5
6
7
8
9
10
// 给参数加类型注解:num1: number,num2: number;
// 给返回值添加类型注解:(): number
// 在开发中,通常情况下可以不写返回值的类型注解(自动推导)
function sum(num1: number, num2: number): number {
return num1 + num2;
}

sum(10, 20); // ok
//sum('aaa', 'bbb') // Error
export {};

匿名函数的参数

  • 匿名函数与函数声明会有一些不同:
    • 当一个函数出现在 TypeScript 可以确定该函数会被如何调用的地方时;
    • 该函数的参数会自动指定类型;
1
2
3
4
5
6
7
// 通常情况下,在定义一个函数时,都会给参数添加类型注解
function foo(message: string) {}

const names = ["why", "kobe", "james"];
names.forEach(function (item) {
console.log(item.split(""));
});
  • 我们并没有指定 item 的类型,但是 item 是一个 string 类型:
    • 这是因为 TypeScript 会根据 forEach 函数的类型以及数组的类型推断出 item 的类型;
    • 这个过程称之为上下文类型(contextual typing),因为函数执行的上下文可以帮助确定参数和返回值的类型;

对象类型

如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?
我们可以使用对象类型;

1
2
3
4
5
6
7
8
9
//Point: x/y ->对象类型
// 对象类型可以用分号(;)分割也可以用逗号(,)分割
// {x:number;y:number}
function printPoint(point: { x: number; y: number }) {
console.log(point.x);
console.log(point.y);
}

printPoint({ x: 123, y: 321 });

在这里我们使用了一个对象来作为类型:
在对象我们可以添加属性,并且告知 TypeScript 该属性需要是什么类型, 属性之间可以使用 , 或者;来分割,最后一个分隔符是可选的; 每个属性的类型部分也是可选的,如果不指定,那么就是 any 类型;

可选类型

在参数后面添加?: 声明的类型注解是可选类型

1
2
3
4
5
6
7
8
9
10
11
12
//Point: x/y/z ->对象类型
// 对象类型可以用分号(;)分割也可以用逗号(,)分割
// {x:number;y:number,z?:number}
// ?: 声明的类型注解是可选类型
// 如果可选类型没有传值的话默认是 undefined
function printPoint(point: { x: number; y: number; z?: number }) {
console.log(point.x); // 123
console.log(point.y); // 321
console.log(point.z); // undefined
}

printPoint({ x: 123, y: 321 });

联合类型

  1. 基础类型联合
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // number |string 联合类型
    function printID(id: number | string) {
    //使用联合类型的值时,需要特别小心
    // narrow:缩小
    if (typeof id === "string") {
    console.log(id.toUpperCase());
    } else {
    console.log("你的id是:" + id);
    }
    }

    printID(15);
    printID("dfd");

2.对象类型联合

对象联合类型只能访问联合中所有共同成员

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Women{
age: number,
sex: string,
cry(): void
}
interface Man{
age: number,
sex: string,
}
declare function People(): Women | Man;
let people = People();
people.age = 18; //ok
people.cry();//error 非共同成员

可选类型和联合类型的关系

可选类型可以看做是 类型 和 undefined 的联合类型:

1
2
3
4
5
6
7
8
9
10
// 一个参数是一个可选类型的时候,它其实类似于是 类型|undefined 的联合类型
// function print(message?: string) {
// console.log(message)
// }
// print()

function foo(message: string | undefined) {
console.log(message);
}
foo(undefined);

类型的别名

前面我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。
比如我们可以给对象类型起一个别名:

1
2
3
4
5
6
7
8
9
10
// type: 用于定义类型别名(type alias)
type IDtype = string | number;
type Point = {
x: 123;
y: 321;
};
function printID(id: IDtype) {
console.log(id);
}
function printPoint(point: Point) {}

类型断言

有时候 TypeScript 无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。
比如我们通过 document.getElementById,TypeScript 只知道该函数会返回 HTMLElement ,但并不知道它 具体的类型

1
2
3
4
// 类型断言 as
// TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型,此规则可防止不可能的强制转换:
const el = document.getElementById("haha") as HTMLImageElement;
el.src = "url地址";

非空类型断言

当我们编写下面的代码时,在执行 ts 的编译阶段会报错:
这是因为传入的 message 有可能是为 undefined 的,这个时候是不能执行方法的;

1
2
3
4
function pringMessageLength(message?: string) {
console.log(message.length);
}
pringMessageLength("Hello World");

如果我们确定传入的参数是有值,这个时候我们可以使用非空类型断言
非空类型断言使用是(!),表示可以确定某个标识符是有值的,跳过 ts 在编译阶段对它的检测;

1
2
3
4
function pringMessageLength(message?: string) {
console.log(message?.length);
}
pringMessageLength("Hello World");

可选链的使用

可选链事实上并不是 TypeScript 独有的特性,它是 ES11(ES2020)中增加的特性.
可选链使用可选链操作符 ?.
它的作用是当对象的属性不存在时,会短路,直接返回 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
28
29
30
type Person = {
name: string;
friend?: {
name: string;
age?: number;
girlFrend?: {
name: string;
};
};
};

const info: Person = {
name: "why",
friend: {
name: "james",
age: 19,
},
};

// 不加可选操作符
// console.log(info.name) // why
// console.log(info.friend.name) // Error
// 如果没有值
// console.log(info.friend.girlFrend.name) // Error

// 加可选操作符
// console.log(info.friend?.name) // james
// console.log(info.friend?.girlFrend?.name) // 古力娜扎
// 如果没有值
console.log(info.friend?.girlFrend?.name); // undefined

(!!) 操作符

将一个其他类型转换成 Boolean 类型,类似于 Boolean(变量)的方式

1
2
3
4
5
let message = "hello world";
// !! 将一个其他类型转换成Boolean类型,类似于Boolean(变量)的方式
let flag = !!message;
// let flag = Boolean(message)
console.log(flag);

(??) 操作符

空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数, 否则返回左侧操作数。

1
2
3
4
5
let message: string | null = null;
// 和下方三元运算符同等效果
const content = message ?? "你好啊,张三";
// const content = message ? message : '你好啊,张三'
console.log(content);

字面量类型

字面量类型 (literal types)

1
2
// 字面量类型
let message: "Hello World" = "Hello World";

字面量类型的意义,就是必须结合联合类型

1
2
3
4
5
type Alignment = "left" | "right" | "center";
let align: Alignment = "left";
align = "center"; // ok
align = "right"; // ok
// align = 'hahah' // Error

字面量推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// const info = {
// url: 'http://www.baidu.com',
// method: 'POST'
// }

// function request(url: string, method: 'GET' | 'POST') {

// }

// request(info.url, info.method) // Error 原因是string类型不可以赋值给字面量类型

type Method = "GET" | "POST";
type Request = {
url: string;
method: Method;
};
const info: Request = {
url: "http://www.baidu.com",
method: "GET",
};
function request(url: string, method: Method) {}
request(info.url, info.method); // ok

类型缩(类型守卫)

什么是类型缩小呢?

  • 类型缩小的英文是 Type Narrowing;
  • 我们可以通过类似于 typeof padding === “number” 的判断语句,来改变 TypeScript 的执行路径;
  • 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小;
  • 而我们编写的 typeof padding === “number 可以称之为 类型保护(type guards)

常见的类型保护有如下几种:

  • typeof
  • 平等缩小(比如===、!==)
  • instanceof
  • in
  • 等等…

typeof

1
2
3
4
5
6
7
8
// 1. typeof缩小
type IDType = number | string;

function printID(id: IDType) {
if (typeof id === "string") {
console.log(id.toUpperCase());
}
}

平等缩小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 2. 平等缩小 (===,==,!==,!=,switch)
type Direction = "left" | "center" | "right";
function printDirection(direction: Direction) {
// 1. if 判断
if (direction === "left") {
console.log(direction);
} else if (direction === "center") {
console.log(direction);
} else {
console.log(direction);
}

// 2. switch 判断
// switch (direction) {
// case 'left':
// console.log(direction);
// break;
// case 'center':
// console.log(direction);
// break;
// default:
// console.log(direction);
// }
}

instanceof

JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”

1
2
3
4
5
6
7
8
// 3. instanceof
function printTime(time: string | Date) {
if (time instanceof Date) {
console.log(time.toUTCString());
} else {
console.log(time);
}
}

in

Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in 运算符,如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true

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
// 4. in
type Fish = {
swimming: () => void;
};
type Dog = {
running: () => void;
};

function move(animal: Fish | Dog) {
if ("swimming" in animal) {
animal.swimming();
} else {
animal.running();
}
}

const fish: Fish = {
swimming() {
console.log("swimming");
},
};

const dog: Dog = {
running() {
console.log("running");
},
};

七、TypeScript 函数详解

在 JavaScript 开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进 行传递)。
那么在使用函数的过程中,函数是否也可以有自己的类型呢?
我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;

函数的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 函数作为参数时,在参数中如何编写类型
function foo() {}

type FooFnType = () => void;

function bar(fn: FooFnType) {
fn();
}
// bar(foo)

// 2. 定义常量时,编写函数的类型
type AddFnType = (num1: number, num2: number) => number;
const add: AddFnType = (a, b) => {
return a + b;
};

console.log(add(50, 50));

参数的可选类型

1
2
3
4
5
// 可选类型必须是写在必选类型后面
// y -> undefined | number
function foo(x: number, y?: number) {}
foo(20, 10);
foo(30);

参数的默认值

从 ES6 开始,JavaScript 是支持默认参数的,TypeScript 也是支持默认参数的

1
2
3
4
5
6
// 必传参数 - 有默认值的参数 - 可选参数
function foo(x: number, y: number = 20) {
console.log(x, y);
}
foo(1, 66);
foo(40);

函数的剩余参数

从 ES6 开始,JavaScript 也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。

1
2
3
4
5
6
7
8
9
10
function sum(...nums: number[]) {
let total = 0;
for (const num of nums) {
total += num;
}
return total;
}

const result = sum(20, 40, 30, 50);
console.log(result);

指定 this 的类型

this 的默认推导

1
2
3
4
5
6
7
8
// this是可以被推导出来 info对象(TypeScript推导出来)
const info = {
name: "ltt",
eating() {
console.log(this.name + "eating");
},
};
info.eating();

this 不明确类型

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

function eating(this: ThisType, message: string) {
console.log(this.name + " eating", message);
}

const info = {
name: "why",
eating,
};

// 隐式绑定
info.eating("哈哈哈");

// 显示绑定
eating.call({ name: "kobe" }, "呵呵呵");
eating.apply({ name: "james" }, ["嘿嘿嘿"]);

函数的重载

在 TypeScript 中,如果我们编写了一个 add 函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?

我们可能会这样来编写,但是其实是错误的

qGDcHH.png
那么这个代码应该如何去编写呢?

  • 在 TypeScript 中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行 调用;
  • 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;

函数的重载 (联合类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 通过联合类型有两个缺点:
* 1.进行很多的逻辑判断(类型缩小)
* 2.返回的类型依然是不确定的
*
*/

function add(a1: string | number, a2: string | number) {
if (typeof a1 === "string" && typeof a2 === "string") {
return a1 + a2;
} else if (typeof a1 === "number" && typeof a2 === "number") {
return a1 + a2;
}
}

函数的重载 (函数重载)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数重载:函数的名字相同,但是参数不相同的几个函数,就是函数的重载
function add(num1: number, num2: number): number; // 没有函数体
function add(num1: string, num2: string): string; // 没有函数体
function add(num1: any, num2: any): any {
return num1 + num2;
}

const result = add(20, 30);
console.log(result);
const result2 = add("aaa", "bbb");

// 在函数的重载中,实现体的函数是不能被直接调用的
// const result3 = add({ name: 'giao' }, { age: 18 }) // Error

八、TypeScript 类的使用

在早期的 JavaScript 开发中(ES5)我们需要通过函数和原型链来实现类和继承,从 ES6 开始,引入了 class 关键字,可以 更加方便的定义和使用类。

TypeScript 作为 JavaScript 的超集,也是支持使用 class 关键字的,并且还可以对类的属性和方法等进行静态类型检测。

实际上在 JavaScript 的开发过程中,我们更加习惯于函数式编程:

  • 比如 React 开发中,目前更多使用的函数组件以及结合 Hook 的开发模式;
  • 比如在 Vue3 开发中,目前也更加推崇使用 Composition API;

但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。

类的封装

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

eating() {
console.log(this.name + "eating");
}
}
const p = new Person("coder_single", 18);
console.log(p.name);
console.log(p.age);
p.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
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log("Person eating");
}
}

class Student extends Person {
sno: number;
constructor(name: string, age: number, sno: number) {
// super 调用父类构造器
super(name, age);
this.sno = sno;
}
studying() {
console.log("studying");
}
// 子类从写方法
eating() {
// 子类从写方法后还想调用父类方法可以使用super来调用父类方法
super.eating();
console.log("Student eating");
}
}

const s = new Student("coder_singel", 18, 111);
console.log(s.name);
console.log(s.age);
console.log(s.sno);
s.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
class Animal {
action() {
console.log("action");
}
}

class Dog extends Animal {
action() {
console.log("Dog running");
}
}

class Fish extends Animal {
action() {
console.log("Fish swimming");
}
}

// animal: dog/fish
// 多态的目的是为了写出更加具备通用性的代码
function makeActions(animals: Animal[]) {
animals.forEach((animal) => {
animal.action();
});
}

makeActions([new Dog(), new Fish()]);

成员修饰符

在 TypeScript 中,类的属性和方法支持三种修饰符: public、private、protected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是 public 的;

  • private 修饰的是仅在同一类中可见、私有的属性或方法;

  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;

下面直接颜色三种修饰符的使用:

public

1
2
3
4
5
6
7
8
// public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;

class Person {
public name: string = "coder-single";
}

const p = new Person();
console.log(p.name);

private

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// private 修饰的是仅在同一类中可见、私有的属性或方法;

class Person {
private name: string = "coderwhy";

// 类方法
getName() {
console.log(this.name);
}
setName(name: string) {
this.name = name;
}
}

const p = new Person();
// console.log(p.name) //Error
p.getName(); // coderwhy
p.setName("single");
p.getName(); // single

protected

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
class Person {
protected name: string = "single";
getName() {
console.log("person", this.name);
}
}
class Student extends Person {
getName() {
console.log("student", this.name);
}
}
const p = new Person();
const s = new Student();
// console.log(p.name) // Error
p.getName(); // ok
s.getName(); // ok

只读属性 redonly

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用 readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// readonly 只读属性 不允许外界随意进行修改
class Person {
// 1. 只读属性可以在构造器中赋值,赋值之后就不可以修改
// 2. 只读本身不能进行修改,但是如果它是对象类型,对象中的属性可以修改
readonly name: string;
readonly friend?: Person;
constructor(name: string, friden?: Person) {
this.name = name;
this.friend = friden;
}
}

const p = new Person("single", new Person("kobe"));
console.log(p.name);
console.log(p.friend);
// p.name = 'aaa' // 不能修改

getters/setters

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

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

// 访问器setter/getter
// setter
set name(newName) {
this._name = newName;
}
// getter
get name() {
return this._name;
}
}
const p = new Person("giao");
p.name = "coderwhy";
console.log(p.name);

类的静态成员

前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法。
在 TypeScript 中通过关键字 static 来定义:

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

// const p = new Person();
// p.age

// 前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法。

class Student {
static time: string = "20:00";
static attendClass() {
console.log("去上学");
}
}

console.log(Student.time);
Student.attendClass();

抽象类 abstract

我们知道,继承是多态使用的前提。

  • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。

  • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

什么是 抽象方法? 在 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
function makeArea(shape: Area) {
return shape.getArea();
}

// 抽象类
// 抽象类是不能被实例化的
abstract class Area {
// 抽象方法
abstract getArea();
}

class Circle extends Area {
private radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
getArea() {
return this.radius * this.radius * 3.14;
}
}

class Rectangle extends Area {
private width: number;
private height: number;

constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
const r = new Rectangle(50, 30);
console.log(makeArea(r));
// 如果我们传入抽象类会报错
// console.log(makeArea(new Area()))

类的类型

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

1
2
3
4
5
6
7
8
9
10
class Person {
name: string = "single";
getName() {}
}

const p: Person = new Person();
const p2: Person = {
name: "giao",
getName() {},
};

九、TypeScript 接口的使用

接口的声明

1
2
3
4
5
6
7
8
9
10
// 通过类型(type)别名来声明对象类型
// type InfoType = {name:string,age:number}

// 另一种方式声明对象类型:接口interface
// 也可以定义可选类型
// 也可以定义只读属性
interface IInfoType {
name: string;
age: number;
}

可选类型

接口中也可以定义可选类型

1
2
3
4
5
6
7
interface IInfoType {
name: string;
age: number;
friend?: {
name: string;
};
}

只读类型

接口中也可以定义只读属性:

这样就意味着我们再初始化之后,这个值是不可以被修改的;

1
2
3
4
interface IInfoType {
readonly name: string;
age: number;
}

索引类型

1
2
3
4
5
6
7
8
9
10
11
12
// type IndexLanguage = {
// [index: number]: string
// }
interface IndexLanguage {
[index: number]: string;
}
const FrontLanguage: IndexLanguage = {
0: "HTML",
1: "CSS",
2: "JavaScript",
3: "Vue",
};

函数类型

前面我们都是通过 interface 来定义对象中普通的属性和方法的,实际上它也可以用来定义函数类型:

1
2
3
4
5
6
7
interface ISumType {
(n1: number, n2: number): number;
}
const sum: ISumType = (n1, n2) => {
return n1 + n2;
};
console.log(sum(40, 33));

接口继承

接口和类一样是可以进行继承的,也是使用 extends 关键字:

并且接口是支持多继承的(类不支持多继承)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person {
name: string;
eating: () => void;
}
interface Student extends Person {
sno: number;
studying: () => void;
}

const stu: Student = {
name: "single",
sno: 118,
eating() {},
studying() {},
};

交叉类型

多种类型的集合,联合对象将具有所联合类型的所有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
interface People {
age: number,
height: number
}
interface Man{
sex: string
}
const lilei = (man: People & Man) => {
console.log(man.age)
console.log(man.height)
console.log(man.sex)
}
lilei({age: 18,height: 180,sex: 'male'});

接口的实现

interface 和 type 的区别

  • 我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?

    • 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
  • 如果是定义对象类型,那么他们是有区别的:

    • interface 可以重复的对某个接口来定义属性和方法;

    • 而type定义的是别名,别名是不能重复的;