TypeScript 是什么?

  • TypeScript 是 JavaScript 的超集,具有可选的类型并且可编译成纯 JavaScript.
  • TypeScript 提供的静态类型系统,在编译阶段通过类型检测,可以避免很多线上(运行时)的 bug.
  • so…. TypeScript 可以写出更易读、更健壮、更可维护的大型项目

image.png

安装和编译

ts 代码是不能直接在浏览器环境或者 node 环境下运行。我们需要将它编译成普通 js 代码。

全局安装 & 编译

  1. npm install typescript -g ## 全局安装;mac可能需要sudo
  2. tsc hello.ts ## 对.ts文件进行编译
  3. tsc hello1.ts hello2.ts ## 多个ts同时编译
  4. node hello.js ## 执行编译后的js代码
  5. tsc --watch ## 文件保存自动编译
  6. tsc --init ## 生成配置文件。当然,你也可以手动创建 tsconfig.json

学习过程中,编译成 ts 还需要 node 命令去执行 js 文件。是不是很麻烦?

  1. npm install ts-node -g ## ts编译 & node执行 一步到位
  2. ts-node hello.ts

当我们尝试去 ts-node hello.ts 时,可能会报错 'Error: Cannot find module '@types/node/package.json' 。 此时我们再装一个即可 npm install -g @types/node

重启 iterm2,此时就可以继续 ts-node hello.ts

在学习开发的过程中,经常会遇到编辑器提示变量被重复申明。
解决方式:
① 换个变量名
② 可以在 ts 文件中添加 export {}

基础数据类型

TypeScript 中一个叫类型,一个叫值。一般冒号后面的为类型,等号后面的为值。

Boolean 类型

  1. const isDone: boolean = true

Number 类型

  1. const age: number = 18

String 类型

  1. const name: string = 'cc'

Array 类型

  1. // 实现方式一:在类型后面加上 []
  2. const list: number[] = [1, 2, 3]
  3. // 实现方式二:数组泛型
  4. const list1: Array<number> = [1, 2, 3]

Tuple 元组类型

元组类型:用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。

  1. const p: [string, number] = ['cc', 18];
  2. // 关于越界的元素。也就是在已知的 tuple 去添加新的元素时,只能添加已有类型的集合
  3. // 例如 上面的例子 只能是 string | number
  4. p.push('beijing'); // ok
  5. p.push(true); // no ok

那么问题来了。 数组 vs 元组

元组 数组
每一项可以是不同的类型 每一项都是同一类型(联合类型除外)
有预定义的长度 没有长度限制
用于表示一个固定的结构 用于表示一个列表

Tuple 实际应用场景:Excel ?

Enum 枚举类型

考虑某个变量所有可能存在的值,用自然语言的单词去表示它每一个值。清晰地表达意图。
比如:颜色、方位、星期等等

普通枚举

  1. enum Direction {
  2. LEFT,
  3. RIGHT,
  4. TOP,
  5. BOTTOM
  6. }

来瞅瞅以上枚举代码编译成 js 长啥样。

  1. var Direction;
  2. (function (Direction) {
  3. Direction[Direction["LEFT"] = 0] = "LEFT";
  4. Direction[Direction["RIGHT"] = 1] = "RIGHT";
  5. Direction[Direction["TOP"] = 2] = "TOP";
  6. Direction[Direction["BOTTOM"] = 3] = "BOTTOM";
  7. })(Direction || (Direction = {}));
  8. // 枚举值 和 枚举名会进行一个反向映射. 例如
  9. console.log(Direction['LEFT'] == Direction[0]); // true
  10. console.log(Direction[0] == 'LEFT'); // true;

默认情况下。
第一个值为 LEFT 为 0,其他枚举成员会开始递增 +1;
Direction[0] 为 LEFT;
与此同时枚举值和枚举名会反向映射。 Direction[‘LEFT’] 为 0。

可以给任意枚举成员设置初始值。

  1. enum Direction {
  2. LEFT,
  3. RIGHT = 6,
  4. TOP,
  5. BOTTOM
  6. }

编译一下结果一目了然。第一个值还是默认为0。第二值被初始化为6。剩下未手动赋值的枚举成员会接着上一个枚举项递增 +1

  1. var Direction;
  2. (function (Direction) {
  3. Direction[Direction["LEFT"] = 0] = "LEFT"; // 将 0 赋予 LEFT,然后又将 LEFT 赋予 0
  4. Direction[Direction["RIGHT"] = 6] = "RIGHT";
  5. Direction[Direction["TOP"] = 7] = "TOP";
  6. Direction[Direction["BOTTOM"] = 8] = "BOTTOM";
  7. })(Direction || (Direction = {}));

注意避免覆盖的情况, 例如

  1. enum Direction {
  2. LEFT = 2,
  3. RIGHT = 1,
  4. TOP,
  5. BOTTOM
  6. }
  7. // 按照逻辑,未手动赋值的枚举成员会接着上一个枚举项递增 +1
  8. // 也就是 此时的 TOP 应该是 2。就与 LEFT 的枚举值重复了。
  9. // ts 并不会报错。 所以我们在使用枚举的时候尽量避免这种情况。

初始值除了整数。小数/负数、字符串可不可以?可以!try it!

  1. enum Direction {
  2. LEFT = 'LEFT',
  3. RIGHT = 6,
  4. TOP = 1.2, // 未手动赋值的前一项 必须为数值类型
  5. BOTTOM // 自然 +1 => 2.2
  6. }

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。简单的说,我们前面的例子都是属于常数项,要么就是直接赋值,要么就是默认值。那么什么是计算所得项呢?

  1. const xx = () => 2
  2. enum Colors {
  3. RED,
  4. YELLOW,
  5. BLUE = 'blue'.length,
  6. GREEN = xx()
  7. }

字符串的长度是不是需要计算一下子,函数执行是不是也得算算。特别要注意一点儿的是,计算所得项后面不能跟没有赋值的枚举项如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错。就是计算所得项后面的枚举项必须指明,可继续添加计算所得项或者添加枚举值(数值类型)。

了解常数枚举
常数枚举与普通枚举的区别就是,它会在编译阶段被删除,并且不能包含计算成员

  1. // const 声明的枚举类型
  2. const enum Colors {
  3. Red,
  4. Yellow,
  5. Blue
  6. }
  7. const getColors = [Colors.Red, Colors.Yellow, Colors.Blue];
  8. // var getColors = [0 /* Red */, 1 /* Yellow */, 2 /* Blue */];

Any 任意类型

定义为 any 类型是可以赋值给任意类型,当类型转换遇到困难或者数据结构复杂难以定义可以使用 any。any 给了我们很大的自由,如果所有类型定义为any,TypeScript 其实就失去的它存在的意义了。

any 类型太宽松了。很多场景会编写出类型正确编译通过,but 运行时报错的代码。例如

  1. let cc: any;
  2. cc.trim();
  3. new cc();
  4. cc.cc.eat(); // 完全可以通过编译,但是运行明显是会报错

Unknow 类型

在 typescript 中,当我们不确定一个类型是什么类型,可以选择给其声明为 any 或者 unknow。但实际上,ts 更推荐使用 unknown, 因为unknown可以保证类型安全。如果使用 any ,其实是放弃了类型检查。

  1. let cc: unknown;
  2. cc = true; // ok
  3. cc = 'yes'; // ok
  4. cc = 100; // ok

我们发现类型 unknown 和 any 。对它进行任何类型的赋值都是正确的。 但是我们尝试将 unknown 赋值给其他类型值会怎么样呢?

  1. let cc: unknown;
  2. let a: unknown = cc; // ok
  3. let b: any = cc; // ok
  4. let c: string = cc; // no ok
  5. let d: number = cc; // no ok
  6. // ...

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。

  1. let cc: unknown;
  2. cc.trim(); // no ok
  3. new cc(); // no ok
  4. cc.cc.eat(); // no ok

对比后我们知道,any 可以随意取值赋值操作,并不会进行任何类型的检查。但是 unknown 就不一样了,它需要先进行断言或者typeof等来进行判断类型

  1. let str: unknown;
  2. // str.length // 直接取 .length 肯定不ok 。但是 any 可以
  3. // 如果要取 length, 我们可以将 str 断言成 string, 或者 intanceof 判断为 String 类型
  4. if (str instanceof String) {
  5. console.log(str.length) // ok
  6. }
  7. // console.log((str as string).length) // ok

这下我们知道 unknown 为什么是安全的了。它只是指明了类型还未确认,后续还需要去断言,也就是它并未放弃类型检查。

Void 类型

表示没有任何类型。当函数没有返回值的时候,TypeScript 会认为返回值类型为 void

  1. function getName(): void {
  2. console.log('Hello, my name is CC !')
  3. }

还有一个点值得注意的是,变量声明为 void 类型本身就没啥作用,它的值只能是 undefined 或者 null。但是 null 需要将配置文件 strictNullChecks 设为 false.

Null 和 Undefined

nullundefined 是其他类型的子类型。说白就是你可以将 null undefined 赋值给 number string boolean 等类型。

  1. // 需要将配置文件 strictNullChecks 设为 false.
  2. let count: number = 1
  3. count = undefined
  4. count = null

Never 类型

never 表示永远不会出现的值的类型。返回 never 的函数必须存在无法到达的终点

  • 抛出异常
  • 无限循环 ```typescript function error(msg: string): never { throw new Error(msg) }

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

function check(x: string | number) { if (typeof x === ‘number’) { console.log(x) // number } else if (typeof x === ‘string’) { console.log(x) // string } else { console.log(x) // never } }

  1. strictNullChecks 开还关呢? 建议打开。严格的校验能让代码更加的健壮。
  2. <a name="aosp0"></a>
  3. ###
  4. <a name="lrlPd"></a>
  5. ## 类型推论
  6. 有时候没有明确指明类型,但是 ts 还是能帮我们推断出一个类型
  7. ```typescript
  8. let count = 100 // ts 推断出 count 是一个 number 类型
  9. count = '100' // 编译报错

有一个要特别注意的是,当在声明变量的时候没有给变量赋值时,该变量会被推断成 any 类型。从而跳过类型检查。

  1. let x
  2. x = 1
  3. x = '1'
  4. // 这段代码完全 ok 的。 编译时不会报错

联合类型

联合类型表示取值可以为多种类型中的某一种。

  1. let money: number | string
  2. money = 10000
  3. money = '1w'

当 ts 还不知道联合类型变量到底是哪一种变量时候,那么我们只能访问该联合类型共有的属性和方法。

  1. function getMoney(money: string | number) {
  2. console.log(money.length) // 编译报错,因为 number 没有length 属性
  3. console.log(money.toString()) // 编译成功,因为 toString 是 number 和 string 的共有方法
  4. }

交叉类型

类型断言

  • 类型断言可以将一个联合类型的变量,指定为一个更加具体的类型
  • 不能将联合类型断言成不存在的类型,当然除了 any

有时候你会比 TypeScript 更了解某个值的详细信息。通常发生在你会更新清楚地知道一个比它现在更确切的类型。需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。

断言的两种写法

  • 值 as 类型
  • <类型>值 (在React开发中不采用这种方式,因为在React中<>会被认为是一个ReactElement)
    1. function getMoney(money: string | number) {
    2. console.log((money as number).toFixed(2))
    3. console.log((money as string).length)
    4. console.log((money as boolean)) // 断言成不存在的值, no ok
    5. console.log((money as any as boolean)) // 通过 双重断言 欺骗编译器
    6. console.log((money as any))
    7. }

    类型别名

    类型别名用来给一个类型起个新名字。 关键词 type ,常用于联合类型。
    1. type Msg = string | string []
    2. function toast(msg: Msg) {
    3. // ...
    4. }
    我们类型别名和接口很像,那有什么差异呢?官网说:

    Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.(因为完美的软件是对拓展开放,所以只要可能,你通常更需要使用接口而不是别名类型。)
    If you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.(如果你无法用接口表示类型,同时你需要使用联合类型或元组,那么别名类型会是经常使用的方式。)

小结一下:

  • 优先使用接口
  • 如果需要联合类型或者元组等类型处理,则可以使用别名类型声明
  • 当不涉及接口继承(extends)、类实现(implement)时,接口和别名只作为类型定义时,两者皆可。

字面量类型

字面量类型来约束只能取某几个字符串中的一个

  1. let direction: 'LEFT';
  2. direction = 'LEFT'; // 只能是 LEFT
  3. type Direction = 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM' // 类型别名
  4. function getDirection(d: Direction): void {
  5. console.log(d)
  6. }
  7. getDirection('RIGHT') // 传入的参数值只能 Direction 中的一个

类型别名 和 字面量类型都是用 type 进行定义

类型守卫

函数类型

函数类型可以指定参数个数、参数类型和返回值类型。

  1. function greeting(name: string): string {
  2. return 'Hello, ' + name
  3. }

函数表达式

  1. type Greeting = (name: string) => string // 此处的箭头表示的是函数返回类型
  2. const greeting: Greeting = (name: string): string => {
  3. return 'Hello, ' + name
  4. }

可选参数

  1. function greeting(name: string, age?: number): void {}

默认参数

  1. function ajax(url: string, method:string = 'GET'):void {}

剩余参数

  1. function pushArray(arr: number[], ...items: number[]): number[] {
  2. items.forEach(item => arr.push(item))
  3. return arr
  4. }

函数的重载
java 函数重载:函数名相同,参数不同,返回类型可以相同也可以不同。
ts 函数重载:以相同函数名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同) 来创建多个函数的方式,当调用函数时,根据实参的形式,选择与它匹配的方法进行执行。

  1. type addType = string | number;
  2. function add(a: number, b: number): number; // 函数声明,属于重载列表
  3. function add(a: string, b: string): string; // 函数声明,属于重载列表
  4. function add(a: addType, b: addType): addType { // 函数实现,紧跟在函数声明后面,不属于重载列表
  5. if (typeof a === 'string' || typeof b === 'string') {
  6. return a.toString() + b.toString()
  7. }
  8. return a + b;
  9. }
  10. add(1, 2);
  11. add('1', '2');
  12. add(1, '2'); // 编译报错
  13. add('1', 2); // 编译报错

注意:

  • 重载可以理解为一个函数提供多个函数定义,最后一个函数必须为函数的实现,并且必须紧跟在函数定义后面。
  • 当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,多个函数定义如果有包含关系,一定要把最精确的定义放在最前面。

当然,除了普通函数有重载,类的方法也有重载。

重写 vs 重载 ?

  • 重写是指子类重写继承自父类中的方法
  • 重载是指为同一个函数提供多个类型定义

在 ES6 以前我们是通过构造函数来实现「类」的概念。ES6 之后,引入了 class。TypeScript 除了 ES6 中类的功能外,还加入了一些新的用法。

其实和 ES6 中类的基本定义和用法都一样的, 比如类的定义、构造函数的定义、类的继承、存储器的用法、静态方法、实例的生成都是一毛一样的用法。TypeScript 可能加入之前说 方法的重载,引入了 pulic、protected等修饰符。

类的基本定义

  • class 定义类
  • constructor 定义构造函数
  • new 生成实例,并且会自动调用 构造函数 ```typescript class User { myName: string; // 当需要 this.myName 时,就需要先在这里定义好该属性。 myAge: number; myCity: string; constructor(myName: string, myAge: number, myCity: string) {
    1. this.myName = myName;
    this.myAge = myAge; this.myCity = myCity; } getName(): void {
    1. console.log(this.myName)
    } }

const u1 = new User(‘cc’, 10, ‘beijing’) u1.getName(); console.log(u1.myAge);

  1. 以上类定义可以通过 public 这个关键词来简化。看一下是不是省掉很多行代码了。
  2. ```typescript
  3. class User {
  4. // 等同于:类中定义该属性,同时给该属性赋值,使代码更简洁了
  5. // 此处如果不写类型会报: 参数“myCity”隐式具有“any”类型
  6. // 可以将 tscofig.js noImplicitAny 改为 false。但不太建议,还是乖乖写上类型吧
  7. constructor(public myName: string, public myAge: number, public myCity: string) {}
  8. getName(): void {
  9. console.log(this.myName)
  10. }
  11. }
  12. const u1 = new User('cc', 10, 'beijing')
  13. u1.getName(); // cc
  14. console.log(u1.myAge); // 10

定义存取器

  1. class User {
  2. constructor(public myName: string) {}
  3. get name() {
  4. return this.myName;
  5. }
  6. set name(newName) {
  7. this.myName = newName;
  8. }
  9. }
  10. const u1 = new User('cc')
  11. console.log(u1.name); // cc
  12. u1.name = 'cpc';
  13. console.log(u1.name); // cpc

静态属性、静态方法

在类里面通过 static 来实现静态属性和静态方法。静态属性和静态方法的调用或者访问方式,是直接 类.静态属性类.静态方法

  1. class User {
  2. static mood: string = 'happy';
  3. static getMood(): string {
  4. return this.mood;
  5. }
  6. }
  7. console.log(User.mood); // happy
  8. console.log(User.getMood()); // happy

继承

通过 extends 关键字来实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。
类的继承包含继承父类的静态(static)、非静态的属性和方法。

  1. class User {
  2. name: string;
  3. age: number;
  4. constructor(name: string, age: number) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. getName(): string {
  9. return this.name;
  10. }
  11. }
  12. // 通过 extends 来实现继承
  13. class Student extends User {
  14. city: string;
  15. constructor(name: string, age: number, city: string) {
  16. super(name, age); // 调用父类中的 constructor(name, age)
  17. this.city = city;
  18. }
  19. greeting(): string {
  20. return 'Hello, ' + super.getName(); // 调用父类中的 getName
  21. }
  22. }
  23. const s1 = new Student('cc', 10, 'beijing');
  24. console.log(s1.getName());
  25. console.log(s1.greeting());

思考: 当子类中存在和父类一样的方法名时,会报错吗?如果不会,调用该方法会执行子类方法还是父类方法呢?(理解为重写父类方法)

继承 vs 多态 ?

  • 继承:子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态:由继承而产生了相关的不同的类,对同一个方法可以有不同的行为

修饰符

修饰符是用来来修饰属性和方法的。

  • public 公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public。(自己、子类和任何地方)
  • private 私有的,不能在声明它的类的外部访问。(只能自己访问)
  • protected 受保护的,它和 private 类似,区别是它在子类中也是允许被访问的。(只能自己和子类访问)
  1. // 此处无代码,请自行脑补。。。

思考:

  • 在类中,属性、方法、参数等没有明确指明修饰符,统统被认为是 public
  • 如果被定义成 private 或者 protected 的属性,在实例想访问该怎么办呢? (gexXXX)
  • 如果 构造函数 添加 protected 或者 private 会是怎样的呢? 静态属性和方法也添加上又会是怎样的呢?

readonly

  • readonly 顾名思义就是只读
  • 可以结合以上的修饰符一起使用,并将其放在修饰符之后, eg. private readonly money: number
  • ts 同样允许将 interface、type、 class 上的属性标识为 readonly
  • readonly vs const : readonly 实际上只是在编译阶段进行代码检查。而 const 则会在运行时检查。
  1. class User {
  2. constructor(public readonly name: string) {}
  3. }
  4. const u1 = new User('cc');
  5. console.log(u1.name); // 读取很ok
  6. u1.name = 'cpc'; // 设置就费劲儿

抽象类

abstract 用于定义抽象类和其中的抽象方法。

  • 抽象类不允许实例化,就是不能 new
  • 抽象类和方法不包含具体实现,必须在子类中实现。能不能不实现,不行,抽象类有几个方法子类就得实现几个方法
  • 抽象方法也只能出现在抽象类中
  • 子类可以对抽象类进行不同的实现
  1. abstract class User {
  2. constructor(public name: string, public age: number) {}
  3. abstract greeting(): string;
  4. abstract eating(): void;
  5. working() { // 抽象类中是可以出现非抽象方法的
  6. console.log('make money!')
  7. }
  8. }
  9. class Teacher extends User {
  10. constructor(name: string, age: number, private gender: string) {
  11. super(name, age);
  12. }
  13. greeting() { // 继承抽象类,抽象方法必须一一实现,缺一不可。
  14. console.log('hello, ' + this.name);
  15. return 'hey man!' // 需和抽象方法返回类型保持一致
  16. }
  17. eating() {
  18. console.log(this.name + '正在吃午饭...');
  19. }
  20. }
  21. const t1 = new Teacher('cc', 10, 'male');
  22. t1.greeting();
  23. t1.eating();
  24. t1.working();

观察一下上面的代码的编译结果:

  1. var __extends = (this && this.__extends) || (function () {
  2. var extendStatics = function (d, b) {
  3. extendStatics = Object.setPrototypeOf ||
  4. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
  5. function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
  6. return extendStatics(d, b);
  7. };
  8. return function (d, b) {
  9. if (typeof b !== "function" && b !== null)
  10. throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
  11. extendStatics(d, b);
  12. function __() { this.constructor = d; }
  13. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  14. };
  15. })();
  16. var User = /** @class */ (function () {
  17. function User(name, age) {
  18. this.name = name;
  19. this.age = age;
  20. }
  21. User.prototype.working = function () {
  22. console.log('make money!');
  23. };
  24. return User;
  25. }());
  26. var Teacher = /** @class */ (function (_super) {
  27. __extends(Teacher, _super);
  28. function Teacher(name, age, gender) {
  29. var _this = _super.call(this, name, age) || this;
  30. _this.gender = gender;
  31. return _this;
  32. }
  33. Teacher.prototype.greeting = function () {
  34. console.log('hello, ' + this.name);
  35. return '';
  36. };
  37. Teacher.prototype.eating = function () {
  38. console.log(this.name + '正在吃午饭...');
  39. };
  40. return Teacher;
  41. }(User));
  42. var t1 = new Teacher('cc', 10, 'male');
  43. t1.greeting();
  44. t1.eating();
  45. t1.working();

我们只看抽象类 User,编译结果中我们发现:

  • 即使是抽象类,仍然会存在这个类;
  • 抽象方法编译结果并不存在;

装饰器

装饰器是啥呢?

  • 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改其行为;
  • 装饰器是一个表达式@expression这种形式,这个表达式被执行后,会返回一个函数;
  • 函数的入参分别为 target、name 和 descriptor;
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象;

说白了,可以将装饰器理解为在原有的代码逻辑外层再加点儿额外的东西(逻辑)。就是装饰一下嘛 !

装饰器可分为:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 参数装饰器

装饰器有哪些写法?

  • 普通装饰器(不可传参)
  • 装饰器工厂(可以传参)

类装饰器

  1. // target: 被装饰的类
  2. declare type ClassDecorator = <TFunction extends Function>(
  3. target: TFunction
  4. ) => TFunction | void;
  1. // 定义一个普通装饰器
  2. function log(target: Function): void {
  3. console.log('logger...')
  4. target.prototype.hello = function(): void {
  5. console.log('hello, cc!')
  6. }
  7. }
  8. // 报错: 对修饰器的实验支持功能在将来的版本中可能更改。在 "tsconfig" 或 "jsconfig" 中设置 "experimentalDecorators" 选项以删除此警告。
  9. // 使用装饰器之前 需要去 tsconfig.json 开一下配置 experimentalDecorators: true
  10. @log // 装饰器的使用方式:紧挨着需要被装饰的类。
  11. class User {
  12. hello!: () => void;
  13. }
  14. const u1 = new User(); // 输出 logger...
  15. u1.hello(); // 输出 hello, cc!

每次实例化的时候都会输出一样的 log。 我们可以自定义传入参数吗? 是可以的。就是前面说的装饰器工厂。

  1. // 装饰器工厂定义一个装饰器
  2. function logFactory(msg: string) {
  3. return function(target: Function): void {
  4. console.log(msg)
  5. }
  6. }
  7. @logFactory('This is a logger...') // 实现传参的方式来执行一个装饰器
  8. class User {}
  9. const u1 = new User();

属性装饰器

  1. // target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. // propertyKey: 被装饰类的属性名
  3. declare type PropertyDecorator = (
  4. target:Object,
  5. propertyKey: string | symbol
  6. ) => void;
  1. function toUpperCase(target: any, propertyKey: string) {
  2. let value = target[propertyKey] // 获取到修饰参数的值
  3. const getter = function() {
  4. return value;
  5. }
  6. const setter = function(newVal: string) {
  7. value = newVal.toUpperCase()
  8. }
  9. // 删除已有属性,添加新的属性值,并把setter也替换了
  10. if (delete target[propertyKey]) {
  11. Object.defineProperty(target, propertyKey, {
  12. get: getter,
  13. set: setter,
  14. enumerable: true,
  15. configurable: true
  16. })
  17. }
  18. }
  19. class User {
  20. @toUpperCase // 注意装饰器后面不能写分号
  21. name = 'cc'
  22. }
  23. const u1 = new User();
  24. console.log(u1.name); // CC

方法装饰器

  1. // target: 被装饰的类
  2. // propertyKey: 方法名
  3. // descriptor: 属性描述符
  4. declare type MethodDecorator = <T>(
  5. target:Object,
  6. propertyKey: string | symbol,
  7. descriptor: TypePropertyDescript<T>
  8. ) => TypedPropertyDescriptor<T> | void;
  1. function MethodDecorator(target: any, property: string, descriptor: PropertyDescriptor) {
  2. console.log(target);
  3. console.log(property);
  4. console.log(descriptor);
  5. }
  6. class User {
  7. name: string = 'cc'
  8. @MethodDecorator
  9. getName() {
  10. console.log(this.name)
  11. }
  12. }

参数装饰器

  1. declare type ParameterDecorator = (
  2. target: Object,
  3. propertyKey: string | symbol,
  4. parameterIndex: number
  5. ) => void
  1. function addAge(target: any, methodName: string, paramsIndex: number) {
  2. console.log(target);
  3. console.log(methodName);
  4. console.log(paramsIndex);
  5. target.age = 10;
  6. }
  7. class User {
  8. age!: number;
  9. login(username: string, @addAge password: string) {
  10. console.log(this.age, username, password);
  11. }
  12. }
  13. const u1 = new User();
  14. u1.login('xxx', '123456')

装饰器执行顺序

  1. /*
  2. 有多个参数装饰器时:从最后一个参数依次向前执行;
  3. 方法和方法参数中参数装饰器先执行;
  4. 类装饰器总是最后执行;
  5. 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行.
  6. */
  7. function Class1Decorator() {
  8. return function (target: any) {
  9. console.log("类1装饰器");
  10. }
  11. }
  12. function Class2Decorator() {
  13. return function (target: any) {
  14. console.log("类2装饰器");
  15. }
  16. }
  17. function MethodDecorator() {
  18. return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
  19. console.log("方法装饰器");
  20. }
  21. }
  22. function Param1Decorator() {
  23. return function (target: any, methodName: string, paramIndex: number) {
  24. console.log("参数1装饰器");
  25. }
  26. }
  27. function Param2Decorator() {
  28. return function (target: any, methodName: string, paramIndex: number) {
  29. console.log("参数2装饰器");
  30. }
  31. }
  32. function PropertyDecorator(name: string) {
  33. return function (target: any, propertyName: string) {
  34. console.log(name + "属性装饰器");
  35. }
  36. }
  37. @Class1Decorator()
  38. @Class2Decorator()
  39. class Person {
  40. @PropertyDecorator('name')
  41. name: string = 'cc';
  42. @PropertyDecorator('age')
  43. age: number = 10;
  44. @MethodDecorator()
  45. greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) { }
  46. }
  47. /**
  48. name属性装饰器
  49. age属性装饰器
  50. 参数2装饰器
  51. 参数1装饰器
  52. 方法装饰器
  53. 类2装饰器
  54. 类1装饰器
  55. */

接口

  • 接口一方面可以在面向对象编程中表示为行为的抽象,另外还可以用来描述对象的形状
  • 接口就是把一些类中共有的属性和方法抽象出来,可以用来约束实现(implements)此接口的类
  • 一个类可以继承(extends)另一个类并实现(implements)多个接口
  • 一个类可以实现多个接口,一个接口也可以被多个类实现,但一个类的可以有多个子类,但只能有一个父类

对象的形状

  1. // 接口可以用来描述`对象的形状`,少属性或者多属性都会报错
  2. interface Person {
  3. name: string;
  4. age: number;
  5. gender?: string; // ? 表示可选属性
  6. speak(msg: string): void;
  7. }
  8. const p1: Person = {
  9. // name: 'cc', // 少属性也会报错
  10. age: 10,
  11. speak(msg: string) {
  12. console.log(msg)
  13. },
  14. address: '', // 多属性报错
  15. }

行为的抽象

  1. // 接口可以在面向对象编程中表示为行为的抽象
  2. interface Eating {
  3. eat(): void;
  4. }
  5. interface Playing {
  6. play(): void;
  7. }
  8. // 一个类可以实现多个接口
  9. class Children implements Eating, Playing {
  10. eat() {}
  11. play() {}
  12. }

任意属性
当我们无法预先知道哪些新的属性,可以这么干

  1. interface Person {
  2. readonly id: number; // 只读
  3. name: string;
  4. gender?: string;
  5. [propName: string]: any
  6. }
  7. // 这里要特别注意的是 [propName: string] 的类型必须包含已有的类型,比如这个例子有 number | string
  8. // 有可选参数时,需要将 undefined 也加上
  9. // 所以这个例子可以这么写 [propName: string]: number | string | undefined
  10. // propName 是随意取的,叫 props 也可,其他也行

接口的继承

  1. interface Eatable {
  2. eat(): void;
  3. }
  4. interface EatKFC extends Eatable {
  5. eatKFC(): void;
  6. }
  7. class WorkHardMan implements EatKFC {
  8. eat() {
  9. console.log('吃点儿东西')
  10. }
  11. eatKFC() {
  12. console.log('努力的人吃KFC')
  13. }
  14. }

思考: 如果定义的两个名字一样的接口会是怎么样? try it. 盲猜不同属性会合并,相同属性类型需要一致

函数类型接口
对函数传入的参数个数、参数类型和函数返回值进行约束。

  1. interface reverseFunc {
  2. (msg: string): string;
  3. }
  4. const reverse: reverseFunc = function(msg: string): string {
  5. return msg.split('').reverse().join('');
  6. }

思考:以下两个接口定义含义分别是什么?

  1. interface T1 {
  2. (name: string): string;
  3. }
  4. interface T2 {
  5. s: (name: string) => string;
  6. }
  7. // T1 描述的是一个函数
  8. // T2 描述的是一个对象,有一个 s 属性,这个属性对应一个函数
  9. const t1: T1 = (name: string): string => name;
  10. const t2: T2 = {
  11. s: (name: string): string => name
  12. }

描述类接口

  1. interface Eating {
  2. name: string;
  3. eat(sth: string): void;
  4. }
  5. class User implements Eating {
  6. constructor(public name: string) {}
  7. eat(sth: string): void {
  8. console.log('正在吃...' + sth)
  9. }
  10. }
  11. const u = new User('cc')
  12. u.eat('ice')

构造函数类型接口
可以使用 interface 里特殊的 new 关键字来描述类的构造函数类型

  1. class Car {
  2. constructor(public size: string) {}
  3. }
  4. interface CarClass {
  5. new(size: string): Car; // 不加 new 是修饰函数的, 加 new 是修饰类的
  6. }
  7. function createCars(clazz: CarClass, size: string) {
  8. return new clazz(size)
  9. }
  10. const car = createCars(Car, 'large')
  11. console.log(car.size)
  12. // 当我们定义一个类时会得到两种类型
  13. // 类的类型 和 实例类型
  14. // 以下代码都是没毛病的
  15. const c: Car = new Car('mini');
  16. const cc: typeof Car = Car;

泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:

  • 类的实例成员
  • 类的方法
  • 函数参数
  • 函数返回值

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

泛型函数

需求:创建一个长度为 length 的数组,数组里面的值用 value 来填充

  1. function createArray(length: number, value: any): any[] {
  2. const res = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value) // i
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 'x')
  9. console.log(res)

思考:以上代码可以编译通过,但是我们发现返回的是一个 any 类型的数组。我们完全可以在函数内部改变修改返回数组的类型。 是不是就缺乏一些约束,或者对应关系。例如当我们传入一个 number 类型,我们只知道任何类型的数组都有可能被返回。。。我们更希望返回数组的类型应该和传入 value 的类型保持一致。

这个时候「泛型」就派上用场了。。。

  1. function createArray<T>(length: number, value: T): T[] {
  2. const res: T[] = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value)
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 'x');
  9. console.log(res)

观察以上泛型代码:
1、我们可以看到如何定义一个泛型。在函数名后面添加一个 ,并在参数、函数返回值、函数内部可以使用到 这个 T 。 so… 可以理解为 T 作用域只限于函数内部使用
2、T 可以理解为类型的形参,我们会在使用的是传递一个具体的类型
3、当然,T 并不是固定了,当然你可以 ABC… 任由你喜欢。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。 下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

4、说好的使用的时候在传递类型,但以上代码并没有看到主动传递类型。因为类型推导会自动推算出来

多个类型参数

在使用泛型的时候,我们可以一次性定义多个类型参数。
需求:我们想定义一个 swap 函数,用来交换输入的元组。

  1. function swap<A, B>(tuple: [A, B]): [B, A] {
  2. return [tuple[1], tuple[0]]
  3. }
  4. console.log(swap([1, '2']))

泛型类

需求:创建一个类,实例方法 push 可以添加值,实例方法会 getRandom 会随机一个已添加过的值

  1. class MyArray<T> {
  2. private arr: T[] = []
  3. // ...
  4. push(value: T) {
  5. this.arr.push(value)
  6. }
  7. getRandom(): T {
  8. return this.arr[Math.floor(Math.random() * this.arr.length)]
  9. }
  10. }
  11. const my = new MyArray();
  12. my.push(1);
  13. my.push(2);
  14. my.push(99);
  15. console.log(my.getRandom())

观察以上代码:
当创建实例的时候并没有主动传入 类型。 此时的 T 是啥类型???

泛型接口

来看一个例子:

  1. interface Calc {
  2. <T>(a: T, b: T): T
  3. }
  4. const sum: Calc = function<T>(a: T, b: T): T {
  5. return a
  6. }
  7. sum<number>(1, 2);

思考:返回的是单个 a, 如果返回的是 a 和 b 的运算结果呢? 运算符“+”不能应用于类型“T”和“T” . 泛型 T 可以是任意类型,因为不知道会传什么东西,所以不能相加。 那咋整?

在来看一个几乎类似的例子:我只是将类型形参 T 提升到了接口上面定义。

  1. interface Calc<T> {
  2. (a: T, b: T): T
  3. }
  4. const sum: Calc<number> = function(a: number, b: number): number {
  5. return a + b;
  6. }
  7. sum(1, 2)

思考:泛型定义在 接口上 和 定义在接口里的函数上 有什么区别呢?
定义在接口上时,我们在使用接口的时候就必须先明确传递一个类型。
定义在接口里的函数时,我们在定义「形状」为该接口的函数时,继续保留 泛型,直到调用该函数时在明确传递类型。

默认泛型

我们之前把泛型理解为类型的形参,当然默认泛型,自然也可以理解为默认参数了

  1. function createArray<T = string>(length: number, value: T): T[] {
  2. const res: T[] = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value)
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 1);

观察以上代码:
是不是有疑惑,默认泛型为 string,创建时候确实没传递具体类型,是不是应该走 string,但传入一个 1 为 number 类型编译却可以通过。

其实最开始的时候我们说过不传递也 ts 也会根据类型推导,知道传递的是什么类型。当可以推导出来类型时,推导 > 默认。

看看另外一个例子,也是之前的例子,泛型类

  1. class MyArray<T = string> {
  2. private arr: T[] = []
  3. // ...
  4. push(value: T) {
  5. this.arr.push(value)
  6. }
  7. getRandom(): T {
  8. return this.arr[Math.floor(Math.random() * this.arr.length)]
  9. }
  10. }
  11. const my = new MyArray(); // 此处没有传递类型,也推导不出具体类型,走了默认类型 string
  12. my.push(1); // 编译报错
  13. my.push(2);
  14. my.push(99);
  15. console.log(my.getRandom())

或者

  1. interface T1<T> {}
  2. type T2 = T1; // 报错 泛型类型“T1<T>”需要 1 个类型参数。
  3. // 方案一:
  4. type T2 = T1<string>
  5. // 方案二:
  6. interface T1<T = string> {}

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

  1. function logger<T>(arg: T): T {
  2. console.log(arg.length); // 并不是任何类型都有 length 属性,所以编译报错
  3. return arg;
  4. }

我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. function logger<T extends Lengthwise>(arg: T): T {
  5. console.log(arg.length);
  6. return arg;
  7. }
  8. logger(1) // 虽然以上的定义编译是没问题的,但是在调用logger时,也要保证传入值有 length 属性

保证有length 属性??? 那我。。。

  1. const obj = {
  2. length: 6
  3. }
  4. type hasLengthObj = typeof obj // 得到一份包含length形状的类型
  5. logger<hasLengthObj>('1')

so。。。 可以理解为类型有一个 length 属性就行 ok 。 不关心你是以什么方式去得到的。

再看 T extends Lengthwise。 T 继承 Lengthwise,T 是 Lengthwise 的子类型,T 需要满足 Lengthwise 的约束条件。可以理解为 T 所拥有的形状只准多,不准少于 Lengthwise 的形状。

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. const obj = {
  5. length: 6,
  6. age: 6
  7. }
  8. type hasLengthObj = typeof obj
  9. let l1: Lengthwise = { length: 2 };
  10. let l2: hasLengthObj = { length: 6, age: 6 };
  11. l1 = l2;
  12. // l2 = l1;

联合类型 和 接口/对象 的区别

泛型工具类型

typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. const man: Person = {
  6. name: 'cc',
  7. age: 10
  8. }
  9. type Human = typeof man // -> Person
  10. const women: Human = {
  11. name: 'xx',
  12. age: 9
  13. }
  1. function toArray(x: number): Array<number> {
  2. return [x];
  3. }
  4. type Func = typeof toArray; // -> (x: number) => number[]

typeof 我的理解是 可以 copy 一份完整的类型格式(或者说形状)。

keyof

keyof 操作符可以用来一个对象中的所有 key 值:

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. type K1 = keyof Person;
  6. type K2 = keyof Person[];
  7. type K3 = keyof { [x: string]: Person }; // 索引签名参数类型必须为 "string" 或 "number"。

in

in 用来遍历枚举类型:

  1. type Keys = "a" | "b" | "c"
  2. type Obj = {
  3. [p in Keys]: any
  4. }

Partial

Partial 的作用就是将某个类型里的属性全部变为可选项 ?。

  1. type Partial<T> = { [P in keyof T]?: T[P] };
  2. interface A {
  3. a1: string;
  4. a2: number;
  5. a3: boolean;
  6. }
  7. type aPartial = Partial<A>;
  8. const a: aPartial = {}; // 不会报错

首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

Readonly

  1. type Readonly<T> = { readonly [P in keyof T]: T[P] };

….Partial、Required、Readonly、Record 和 ReturnType