接口、枚举、泛型

接口的声明

在前面我们通过type可以用来声明一个对象类型,对象的另外一种声明方式就是通过接口来声明:
准确说 ts 中的接口是接口类型,这和 Java 中的接口不一样。Java 中的接口是一个类,可以直接在类中定义方法属性;但是 ts 中的接口是个类型,类型自然不能独立使用,它需要和类一起使用,用来约束类或者和其他的什么一起使用。所以说 interface 的作用,其实定义类型别名的 type 也基本能做到。因为他们都是在定义类型。

  1. // 通过类型(type)别名来声明对象类型
  2. // type InfoType = {name: string, age: number}
  3. // 另外一种方式声明对象类型: 接口interface
  4. // 在其中可以定义可选类型
  5. // 也可以定义只读属性
  6. interface IInfoType {
  7. readonly name: string // 只读属性
  8. age: number
  9. friend?: { // 可选属性
  10. name: string
  11. }
  12. }
  13. const info: IInfoType = {
  14. name: "why",
  15. age: 18,
  16. friend: {
  17. name: "kobe"
  18. }
  19. }
  20. console.log(info.friend?.name)
  21. console.log(info.name)
  22. // info.name = "123"
  23. info.age = 20

索引类型

索引就是对象中的 key,给对象中的属性名也进行类型限制

  1. // 通过interface来定义索引类型
  2. interface IndexLanguage {
  3. [index: number]: string
  4. }
  5. const frontLanguage: IndexLanguage = {
  6. 0: "HTML",
  7. 1: "CSS",
  8. 2: "JavaScript",
  9. 3: "Vue"
  10. }
  11. interface ILanguageYear {
  12. [name: string]: number
  13. }
  14. const languageYear: ILanguageYear = {
  15. "C": 1972,
  16. "Java": 1995,
  17. "JavaScript": 1996,
  18. "TypeScript": 2014
  19. }

函数类型

前面我们都是通过 interface 来定义对象中普通的属性和方法的,实际上它也可以和 type 一样用来定义函数类型:
当然,除非特别的情况,还是推荐大家使用类型别名来定义函数:

  1. // type CalcFn = (n1: number, n2: number) => number
  2. // 可调用的接口
  3. interface CalcFn {
  4. (n1: number, n2: number): number // 函数类型
  5. }
  6. function calc(num1: number, num2: number, calcFn: CalcFn) {
  7. return calcFn(num1, num2)
  8. }
  9. const add: CalcFn = (num1, num2) => {
  10. return num1 + num2
  11. }
  12. calc(20, 30, add)

接口继承

接口和类一样是可以进行继承的,也是使用extends关键字,并且接口是支持多继承的(类不支持多继承)

  1. interface ISwim {
  2. swimming: () => void
  3. }
  4. interface IFly {
  5. flying: () => void
  6. }
  7. interface IAction extends ISwim, IFly {
  8. }
  9. const action: IAction = {
  10. swimming() {
  11. },
  12. flying() {
  13. }
  14. }

接口的实现

接口定义后,可以被类实现。面向接口编程就是只看接口中定义了什么 api,具体的实现不用管,直接使用接口中的东西就行。具体执行的时候就把实现类的实例传入。
接口就好比特殊的抽象类,可以多继承。实现implement也就是特殊的继承。

  1. interface ISwim {
  2. swimming: () => void
  3. }
  4. interface IEat {
  5. eating: () => void
  6. }
  7. // 类实现接口
  8. class Animal {
  9. }
  10. // 继承: 只能实现单继承
  11. // 实现: 实现接口, 类可以实现多个接口
  12. class Fish extends Animal implements ISwim, IEat {
  13. swimming() {
  14. console.log("Fish Swmming")
  15. }
  16. eating() {
  17. console.log("Fish Eating")
  18. }
  19. }
  20. class Person implements ISwim {
  21. swimming() {
  22. console.log("Person Swimming")
  23. }
  24. }
  25. // 面向接口编程:将接口传入即可
  26. function swimAction(swimable: ISwim) {
  27. swimable.swimming()
  28. }
  29. // 1.所有实现了接口的类对应的对象, 都是可以传入
  30. swimAction(new Fish())
  31. swimAction(new Person())
  32. swimAction({swimming: function() {}})

交叉类型

交叉类型(Intersection Types):交叉类似表示需要满足多个类型的条件,交叉类型使用 &符号;
比如type hhh = string & number,hhh 类型的值需要同时符合 string 类型和 number 类型,没有这样的值,所以其实是个 never 类型。
基本类型基本无法交叉,所以,在开发中,我们进行交叉时,通常是对对象类型进行交叉

  1. // 一种组合类型的方式: 联合类型
  2. type WhyType = number | string
  3. type Direction = "left" | "right" | "center"
  4. // 另一种组件类型的方式: 交叉类型
  5. type WType = number & string
  6. interface ISwim {
  7. swimming: () => void
  8. }
  9. interface IFly {
  10. flying: () => void
  11. }
  12. type MyType1 = ISwim | IFly
  13. type MyType2 = ISwim & IFly
  14. const obj1: MyType1 = {
  15. flying() {
  16. }
  17. }
  18. const obj2: MyType2 = {
  19. swimming() {
  20. },
  21. flying() {
  22. }
  23. }

interface 和 type 区别

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

  • 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;

如果是定义对象类型,那么他们是有区别的:

  • interface 可以重复的对某个接口来定义属性和方法
  • 而 type 定义的是别名,别名是不能重复的;

因为可以重名,所以有合并的效果,比如给一些默认的对象添加属性。

  1. // window math 等这些默认对象存在 ts 的库文件中
  2. interface Window {
  3. age: number // 相当于合并了 age 属性到 window 中
  4. }
  5. window.age = 19
  6. console.log(window.age)

字面量赋值

看下面代码

  1. interface IPerson {
  2. name: string
  3. age: number
  4. height: number
  5. }
  6. const info = {
  7. name: "why",
  8. age: 18,
  9. height: 1.88,
  10. address: "广州市"
  11. }
  12. // 直接字面量赋值会报错
  13. const t: IPerson = {
  14. name: "why",
  15. age: 18,
  16. height: 1.88,
  17. address: "广州市" // 因为 Iperson 类型没有 address 属性
  18. }
  19. // 但是拿字面量对象的引用赋值,却不会报错
  20. const p: IPerson = info

这是因为 TypeScript 在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。
但是之后如果我们是将一个变量标识符赋值给其他的变量时,会进行freshness擦除操作。它会擦除掉多余的部分以满足类型校验。
使用场景:比如一个函数接收对象,有擦除操作就可以接收不仅有类型要求的属性,还有多出其他属性的对象。相当于扩大了函数接收对象的范围。并且严格限制了字面量直接传参。

  1. function printInfo(person: IPerson) {
  2. console.log(person)
  3. }
  4. // 代码会报错,严格限制字面量直接传参
  5. printInfo({
  6. name: "why",
  7. age: 18,
  8. height: 1.88,
  9. address: "广州市"
  10. })
  11. // 传入有多余属性的对象引用,正确
  12. printInfo(info)

枚举

枚举就是定义一个类型,这个类型的取值是有限的,只有我们列出来的这几个值。和字面量类型组成联合类型很相似。

  1. // type Direction = "left" | "Right" | "Top" | "Bottom"
  2. enum Direction {
  3. LEFT, // 因为就跟常量一样,所以一般大写
  4. RIGHT,
  5. TOP,
  6. BOTTOM
  7. }
  8. function turnDirection(direction: Direction) {
  9. switch (direction) {
  10. case Direction.LEFT:
  11. console.log("改变角色的方向向左")
  12. break;
  13. case Direction.RIGHT:
  14. console.log("改变角色的方向向右")
  15. break;
  16. case Direction.TOP:
  17. console.log("改变角色的方向向上")
  18. break;
  19. case Direction.BOTTOM:
  20. console.log("改变角色的方向向下")
  21. break;
  22. default:
  23. const foo: never = direction;
  24. break;
  25. }
  26. }
  27. turnDirection(Direction.LEFT)
  28. turnDirection(Direction.RIGHT)
  29. turnDirection(Direction.TOP)
  30. turnDirection(Direction.BOTTOM)

枚举类型的值

列出来的枚举类型的值,其实也是个变量,它的值默认是 0 开始自然递增,并且我们也可以修改它的值。比如将中间枚举值修改为 100,那它之后的枚举值的实际值就是从 101 开始递增。
image.pngimage.pngimage.png

泛型

类型参数化

软件工程的主要目的是构建不仅明确且一致的API,还要让你的代码具有很强的可重用性:比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作;
为了提高通用性,对于参数的类型我们也可以进行参数化。泛型就是类型的参数化。

  1. // 类型的参数化
  2. // 在定义这个函数时, 我不决定这些参数的类型
  3. // 而是让调用者以参数的形式告知,我这里的函数参数应该是什么类型
  4. // 可以传入多个泛型
  5. function sum<Type, E>(hhh: Type, msg: E): Type {
  6. return hhh
  7. }
  8. // 1.调用方式一: 明确的传入类型
  9. sum<number, string>(20, 'qwe')
  10. sum<{name: string}, number>({name: "why"}, 123)
  11. sum<any[]>(["abc"])
  12. // 2.调用方式二: 类型推到
  13. sum(50, 'qwe')
  14. sum("abc", {name: 'zs'})

平时在开发中我们可能会看到一些常用的名称:

  • T:Type的缩写,类型
  • K、V:key和value的缩写,键值对
  • E:Element的缩写,元素
  • O:Object的缩写,对象

    泛型接口

    ```javascript interface IPerson { name: T1 age: T2 }

const p: IPerson = { name: “why”, age: 18 }

  1. <a name="gdn4y"></a>
  2. ## 泛型类
  3. ```javascript
  4. // 给类添加泛型
  5. class Point<T> {
  6. x: T
  7. y: T
  8. z: T
  9. constructor(x: T, y: T, z: T) {
  10. this.x = x
  11. this.y = y
  12. this.z = y
  13. }
  14. }
  15. // 自动推导
  16. const p1 = new Point("1.33.2", "2.22.3", "4.22.1")
  17. // 明确指定
  18. const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")
  19. const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")
  20. const names1: string[] = ["abc", "cba", "nba"]
  21. // 之前定义的数组的方式之一,其实就是类的泛型
  22. const names2: Array<string> = ["abc", "cba", "nba"] // 不推荐(react jsx <>)

泛型约束

有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:

  • 比如 string 和 array 都是有 length 的,或者某些对象也是会有 length 属性的;
  • 那么只要是拥有 length 的属性都可以作为我们的参数类型,那么应该如何操作呢?

我们可以使用extends关键字

  1. interface ILength {
  2. length: number
  3. }
  4. // 泛型继承接口类型,约束了泛型,使得传入的类型必须拥有 length 属性
  5. function getLength<T extends ILength>(arg: T) {
  6. return arg.length
  7. }
  8. getLength("abc")
  9. getLength(["abc", "cba"])
  10. getLength({length: 100})