2023-06-14
TypeScript
00
请注意,本文编写于 536 天前,最后修改于 310 天前,其中某些信息可能已经过时。

目录

常见面试题
字面量类型
枚举类型
常数项和计算所得项
常数枚举
外部枚举
枚举合并
类型别名
type vs interface
交叉类型
索引类型
映射类型
类类型语法
定义类的实例属性有两种方式
获取类类型和实例类型
抽象类
类与接口
泛型
何为泛型
泛型的特性

上一篇文章简单介绍了TS的基本类型和常用类型,这一篇文章讲介绍相对高级一点的类型 枚举type交叉类型映射类型泛型 ,由于TS内容确实很多,本篇案例首先会尽可能精简,其次有些内容比较好理解将不再给案例,力求这几篇文章覆盖TS绝大部分知识点,绝不留下面试盲区或技术漏洞😂。

常见面试题

  1. typeinteface有什么异同?
  2. 枚举、常量枚举、外部枚举他们之间有什么区别?
  3. 接口可以继承类吗?类、接口、抽象类之间的关系
  4. 交叉类型是怎样继承的?
  5. TypeScript 类中成员的 publicprivateprotected修饰符的理解?
  6. 什么是泛型?
  7. 映射类型有哪些用途?

带着问题开始本篇文章的阅读吧

字面量类型

我个人理解 字面量类型是联合类型的一种特殊情况,联合类型可以联合基本类型和非基本类型,而字面量类型只有三种

  • 字符串字面量
  • 数字字面量
  • 布尔字面量
ts
let color: "red" | "green" | "blue"; // 字符串字面量类型 let number: 1 | 2 | 3; // 数字字面量类型 let bool: true; // 布尔字面量类型 color = "red"; // 可以赋值为 "red"、"green" 或 "blue" // 错误示例,因为类型不匹配 color = "yellow"; // 类型错误,只能赋值为 "red"、"green" 或 "blue" // 字面量类型与联合类型之间的关系 type Colors = "red" | "green" | "blue"; // 字面量类型 type UniType = "red" | "green" | "blue" | "yellow"; // 联合类型 function getColor(colors: Colors, uniType: UniType) { // colors = uniType; // 报错 不能将类型“"yellow"”分配给类型“Colors”。ts(2322) uniType = colors; // 可以因为后者是前者的子集 }

枚举类型

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

  • 使用关键字 enum 来定义 enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
  • 0开始+1递增, 同时会对枚举值到枚举名进行反向映射

image.png

  • 如果手动赋值 要注意枚举值不要重复,另外不建议用小数
  • 手动赋值可以不是数字, 这意味着后续成员要赋值 image.png
  • 枚举值可以引用前面的成员
ts
// 使用枚举值中其他枚举成员 enum Message { Error = "error message", ServerError = Error, } console.log(Message.Error); // 'error message'

常数项和计算所得项

ts
enum Color {Red, Green, Blue = "blue".length}; // 如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错: enum Color {Red = "red".length, Green, Blue}; // index.ts(1,33): error TS1061: Enum member must have initializer.

常数枚举

常数枚举是使用const enum定义的枚举类型

  • 与普通枚举的区别是,他会在编译阶段被删除,使用的地方会替换为值,并且不包含计算成员

image.png

外部枚举

外部枚举是使用declare enum定义的枚举类型

  • ·declare·定义的类型只会在编译时检查, 编译结果中被删除 image.png

枚举合并

  •  这篇文章中说同名枚举类型会合并,我实测了下不行,估计是新版版废弃了

类型别名

接口类型的作用就是将内联类型抽离出来,从而实现类型复用。其实,还可以使用类型别名接收抽离出来的内联类型实现复用。

  • 类型别名会给一个类型起个新名字。老的类型变动(interface重复声明)别名类型也会同步
  • 有一些场景接口无法覆盖只能使用类型别名
    • 原始值、联合类型 type Name = number | string;
    • 交叉类型 type Vegetables = {radius: number} & {length: number}
    • 元组以及其它任何你需要手写的类型。

type vs interface

相同点:

  • 都可以定义对象类型 (包括类数组、函数)
  • 都可以使用泛型 (后面讲泛型)
  • 都可以继承、多继承但方式不同
    • interface通过 extendsimplements实现继承 (后面讲继承)
    • type通过交叉类型进行继承,左右有些区别,type 通过extends编程更方便 (条件及递归) 不同点
  • interface 可以重复声明
  • type 是别名, 原始类型变更会跟着同步变更
  • type 可以用于基本类型、联合类型、交叉类型、元组等需要手写的地方

交叉类型

交叉类型是将多个类型合并为一个类型

  • 合并基本类型、字面量类型 取交集 很可能是never
  • 合并对象类型
    • 会遍历两个对象属性进行合并
    • 相同属性不同值类型, 取交集,很可能是never
    • 可选属性与非可选属性交叉得到必选属性
    • 只读属性和非只读属性交叉得到非只读属性
  • 合并联合类型 --> 交集
ts
interface FirstType { prop1: number; prop2: string; } interface SecondType { prop2: number; prop3: boolean; } type CombinedType = FirstType & SecondType; const obj: CombinedType = { prop2: throwError('--'), prop1: 42, prop3: false, }; function throwError(message: string): never { throw new Error(message); } // 合并联合类型 --> 交集 type A = "blue" | "red" ; type B = 996 | 'red'; type C = A & B // C: 'red' // 可选 只读 必选属性交叉的结果 type AA = InsertToObj<{name?: string} & {name:string}> // 得到 AA: {name:string} type BB = InsertToObj<{readonly name?: string} & {name:string}> // 得到 BB: {name:string} type CC = InsertToObj<{readonly name: string} & {name?:string}> // 得到 CC: {name:string} type DD = InsertToObj<{readonly name: string} & {readonly name?:string}> // 得到 DD: { readonly name:string}

索引类型

实际上,经常会把对象当 Map 映射使用。比如枚举对象的key类型

ts
interface Person { name: string; age: number; } type Keys = keyof Person; // Keys: 'name' | 'age'

如果像要获取对象的值类型怎么办呢?

ts
type ValueType = Person[keyof Person] // ValueType: string | number

需要注意,当使用数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0'0' 索引对象时,这两者等价。

ts
type Student = [string, number] type Values = Student[number]; // Values: string | number interface RoleDic { [id: number]: string; } const role1: RoleDic = { 0: "super_admin", 1: "admin" }; const role3: RoleDic = ["super_admin", "admin"]; // ok role3.length // 报错,因为role3 并非真的数组

映射类型

索引类型是获取对象mapkeyvalue得到一个联合类型,而映射类型通常借助索引类型生成一种新的类型。

ts
readonly Person --> PersonReadonly --> Person interface Person { name: string; } type MyReadonly<T> = { readonly [P in keyof T]: T[P]; } type MyReadonlyRemove<T> = { -readonly [P in keyof T]: T[P]; }

映射类型可以做一些有意思的事情,比如

  • 拆包/包装 如上例的 readonly Person --> PersonReadonly --> Person
  • 同理 可以实现对象所有属性 可选与必选的转换 Partial Require
  • 递归也可以 DeapReadonly
  • 从一个对象类型中增删一些属性,或修改为另一种类型。
  • 还可以将AB两个对象聚合成一个新对象CCAB的全部属性,但B的属性变成可选类型
  • 多个类型之间的集合运算
  • 。。。。。

鉴于映射内容比较多且偏向与类型编程,在下一章节《TypeScript类型编程》 将详细介绍这些案例。

类类型语法

  • 在JS中通过class关键字定义一个类型,在TS中class还表示声明了一种类型
  • 可以使用 public private protected修饰类的实例属性或方法、静态属性或方法及构造参数属性。
    • public 默认就是public 在任何地方都可以访问
    • private 私有的,不允许在类的外部访问
    • protected 受保护的,与private 类似,但它可以在子类中访问。
    • 这三个关键字只能在类中使用,不可以修饰类,不可以其他地方(比如interface)中使用
  • 同样可以使用 readonly ?修饰 类的实例属性或方法,静态属性或方法
ts
class Animal { static say?() {}; readonly name: string; static age?: number; public constructor(name: string) { this.name = name; } } var ani: Animal = new Animal('cat');

定义类的实例属性有两种方式

  • 直接定义
  • 构造器函数参数 增加修饰符
ts
// 定义类的实例属性 class Cat { public name: string; // 方式1 public constructor(name: string) { this.name = name; } } class Cat2 { // 方式2 在构造器函数参数重定义实例属性,必须有修饰符 public constructor(public name: string) { // this.name = name; // 这一行代码不用写转JS时自动生成 } }

获取类类型和实例类型

ts
class Cat { public constructor(public name: string) { this.name = name; } } let cat: Cat = new Cat('Tom'); // 获取类的类型 type MyCat = typeof Cat; const Cat2: MyCat = class { public name: string; public constructor(name: string) { this.name = name; } } const cat2 = new Cat2('Tom') // 获取类的实例类型 type InstanceCat = InstanceType<typeof Cat> const obj: InstanceCat = { name: 'Tom' }

抽象类

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

  • 抽象类是不能被实例化new
  • 抽象类可以没有抽象方法
  • 抽象类的抽象方法必须被子类实现
  • 即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类(要不然JS运行不就出问题了吗)
ts
abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi(); } class Cat extends Animal { public sayHi() { console.log(`Hello, I am ${this.name}.`); } }

抽象类与类之间的关系

  • 类与抽象类的区别 1抽象类不能new 2抽象类可以有抽象方法
  • 类与抽象类可以相互(类与类、类与抽象类、抽象与抽象类)继承
  • 类与抽象类可以相互实现,构造函数要自己填充

类与接口

  • 不同类之间共有的特性,可以提取为接口,类通过implements关键字实现接口。
  • 一个类可以实现多个接口
  • 接口与接口之间可以继承及多继承
  • 接口可以继承类(一般面向对象的语言是不允许的)
  • 类实现接口的属性和方法是 public 特性
ts
// 多个类之间共有的特性可以抽取为接口 // 通过implements实现接口 interface Alarm { alert(): void; } class Door implements Alarm { alert() { console.log('SecurityDoor alert'); } } class Car implements Alarm { alert() { console.log('Car alert'); } } // 一个类可以实现多个接口 interface Come { in(key: string): void; } class Room implements Alarm, Come { in(key: string) { console.log(`you can${key === 'admin' ? ' ' :' not '}come in`); } alert() { console.log('Car alert'); } } // 接口可以继承接口,也可以是多继承 interface Secretary extends Come, Alarm { name: string } const secretary: Secretary = { name: 'songzhe', in() {}, alert() {} } // 接口还可及继承类 interface Baoma extends Car { footer: number; } let bm: Baoma = { footer: 4, alert() {}, }

泛型

何为泛型

泛型的解释有好几个版本

  • 泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性.
  • 声明一种类型既能满足现在的数据类型也能满足未来的数据类型
  • 泛型就是解决类、接口、方法的复用性,以及不特定数据结构的(类型校验)

image.png

泛型的特性

  • 可以指定多个参数
  • 泛型约束:
    • 由于事先不知道他属于那种类型,所以不能随意操作它的属性或方法
    • 可以让通过extends关键字描述泛型的父类, 这样它就能使用父类的方法和属性了
    • 多个泛型之间可以相互约束
  • 接口中使用泛型
  • 别名类型使用泛型
  • 类中使用泛型
  • 泛型也可以用默认值 =
ts
// 泛型约束条件 interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } // 多个泛型参数之间可以相互约束 function copyFields<T extends U, U>(target: T, source: U): T { for (let id in source) { target[id] = (<T>source)[id]; } return target; } let x = { a: 1, b: 2, c: 3, d: 4 }; copyFields(x, { b: 10, d: 20 }); // 上例中,我们使用了两个类型参数,其中要求 T 继承 U, // 这样就保证了 U 上不会出现 T 中不存在的字段。

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!