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

目录

常见面试题
字面量类型
枚举类型
常数项和计算所得项
常数枚举
外部枚举
枚举合并
类型别名
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 许可协议。转载请注明出处!