本节内容预览

这篇文章,主要会接受下面3个的用法和区别:

  1. interface 接口;
  2. class 类;
  3. type 类型别名;

当我们需要描述一个复杂对象的时候,我们该怎么办?这篇文章会为你展开思路。

先来看个例子

  1. let mySquare: Square = { color: 'white', width: 10 };

例子里我们希望自定义个方块类型 Square ,它有2个属性,颜色和大小。
我们来看下,TS 里这个 Square方块类型 可以怎么定义,顺便可以初步预览下TS中的类和接口。

使用 class 类

class Square {
    color: string;
    width: number;
  constructor (
      color: string,
    width: number
  ) {
      this.color = color;
      this.width = width;
  }
}

TS 中的类和 JS 基本写法没有太多区别,无非就是 每个属性上多增加了类型的定义 。其能力其实还是一致的。

使用 interface 接口

interface Square {
  color: string;
    width: number;
}

我们第一次来看这个接口定义。
如果对比上面 的定义来看,也就好理解了。它定义了 Square对象类型 上有2个属性,这2个属性也符合我们例子里的需求。

使用 type 类型别名

type Square = {
  color: string,
    width: number
}

type就更好理解了,直接等于一个对象类型。

分析下三者的区别

我们可以注意到3种方式都可以完成这个方块类型的定义,但是3者有什么区别呢?我们需要抓住他们的特点:

  1. class 其实是对象的类的实现,它可以通过 new Spuare() 的方式直接实例化我们需要的对象;
  2. interface 其实更贴近类型定义的作用,它不能直接实例化;
  3. type 就抓住一个重点,它叫 “别名” ,所以它的作用是为了简化,看定义方式都像一个变量,突出了它的临时性;

那使用的时候我们可以按这么个规则来判断该用那个:

  1. 如果我们需要构造函数,需要类,需要 实现 那必然是 class;
  2. 不适用第一条的时候,即我们不需要 实现 ,我们优先考虑 interface是否能满足我们的需求;
  3. 不适用前2条的时候,我们再使用 type 别名,比如:我们后面会提到的联合类型,使用 type 来减少重复,就是挺常见的办法;

小结和中场休息

通过上面的例子,其实我们已经对 TS 里的类和接口有了一个初步的认识。
熟知 ES中Class的同年甚至可以直接跳过剩下内容。当然继续阅读,然后查漏补缺也是极好的。

因为类和JS已有的Class是完全对应的,所以这里再次强调下:本系列重难点是**类型定义,也就是TS和JS不同的地方。**
所以我们的重点就会在以下2个JS没有的地方:

  1. 属性修饰符
  2. 抽象类

属性修饰符

先来列举下属性修饰符有哪些:

  1. 控制是否能被读取:public公共、private私有、protected保护
  2. 控制是否能被写:readonly 只读,这个可以和上面3个同时使用

怎么区分 public、private、protected 的修饰符呢?

先来个例子看下用法:

class Square {
    public x: number;
  private y: number;
  protected z: number;
}
const mySquare = new Square();

写法非常像装饰器,但是无需@;

然后我们对比下,就可以了解了:

  1. 外部访问(mySquare.x— 这个是 public;
  2. 不能外部访问(mySquare.x):
    1. 被继承后,能内部访问(this.y) — protected;
    2. 被继承后,不能内部访问(this.y) — private;


一个小注意事项:
public 在TS里是默认的,也就是说没有加修饰符的都是 public ** ,你无需特意添加这个修饰符。

readonly 只读修饰符

这个就很好理解了,假如我们为Square的width添加上只读修饰符:

class Square {
    color: string;
    readonly width: number;
  constructor (
      color: string,
    width: number
  ) {
      this.color = color;
      this.width = width; // 完美执行,构造函数里可以赋值
  }
}

const mySquare = new Square('red', 10);
mySquare.width = 99; // 错误,width 是只读的

这个看例子就能懂了~ 无需太多解释。

抽象类

抽象类,扶额,又是一个新概念,扶额;
抽象类它是一个专门用来被继承的基类,一般不会被实例化(TS会报错哦)。
它只有1个关键词 abstract ,但是可以用在2个地方,来个例子~

abstract class Shape {
  abstract color: string;
    abstract getArea (): number;
}

例子中可以看到 abstract 被作用在2个地方:

  1. 写在 class 前面,说明它是一个抽象类;
  2. 写在 抽象类的一个成员前 ,且必须在派生类中实现,可以和属性描述符同时使用;

所以我们写的继承的派生类时,可以这么写:

class Square extends Shape {
    color: string; // 必须得有 color 属性
    width: number;
  constructor (
      color: string,
    width: number
  ) {
      this.color = color;
      this.width = width; 
  }

  // 必须得有 getArea 方法
  getArea () {
    return this.width * this.width;
  }
}

同学们会不会问,那写抽象类干嘛?不是自己给自己找难受么?
那约束的作用就是为了让系统更稳定可靠,不会因为忘记写方法或者属性而出错,这个在实现多态等设计时,会非常有用,毕竟有句话说的好:唯有自律才有自由。

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。那接口就是用来做这些类型的定义。

interface Square {
  color: string;
    width: number;
}

在看下这个例子,用来表示一个方块,它有 2个属性,color颜色和width宽度,这个都还好理解。这也是一个基础的接口。

可选属性

继续上面一个例子

interface Square {
  color?: string;
    width: number;
}

假如我们认为方块的颜色是可选的,不是必须的,那我只需要加一个? 就可以了。

let square1: Square = { width: 10 } // 正确
let square2: Square = { color: '#fff' ,width: 10 } // 正确

看代码就能迅速的知道了,表示 color 它可能有,也可以没有。

只读属性

直接看例子就好,很简单(其实和上文class的只读完全一样

interface Square {
  readonly color: string;
    width: number;
}

let square: Square = { color: 'white', width: 10 };
square.width = 20; // 正确
square.color = 'red'; // 错误

关键点:

  1. 关键词: readonly;
  2. 只读的只能初始化,不能修改;

同样针对数组,如果想只读可以这么写:

let readOnlyArray: ReadonlyArray<number> = [1,2,3];
readOnlyArray[1] = 5; // 错误
readOnlyArray.push(5); // 错误
readOnlyArray.length = 100; // 错误
let a: number[] = readOnlyArray; // 错误

可以防止对数组的各种修改。
第4种,不知道大家有没理解? 来解释下: 因为js的对象都是引用,ts这样的报错可以防止因为引用的修改,而修改了只读的数组。
那有没可以绕过的呢?有! 类型断言(虽然不推荐使用)

let a: Array[] = readOnlyArray as number[];

可索引类型

先来看下可索引的代码是怎么样的:

interface StringObj {
  [key: string]: string;
}

let stringObj: StringObj = { a: 'aaa', b: 'bbb' } // 正确
let stringObj: StringObj = { c: 123 } // 错误

可以看到,它能对所有属性进行约束,但是不会局限于某一个具名的属性。

但是使用时,我们得注意:

  1. 混用字符串和数字的索引时,数字必须是字符串所以的子类型,看2个例子我们来理解下: ```typescript interface Example1 {

[index: number]: number; // 错误 }

interface Example2 {

[index: number]: ‘a’; // 正确 }

**'a' 是 string的子类型, number和 string是完全不同的东西。**

<a name="bJB8q"></a>
### 额外的属性检查
了解完一系列接口属性的写法,我们看下属性检查怎么做的。<br />额外的属性检查只在 对比类型和实际赋值时校验:
```typescript
interface Square {
  color: string;
    width: number;
}

let square: Square = { colorX: 'white', width: 10} // 错误

妥妥的报错,也很好理解,毕竟Square中没有colorX只有color

但是有个很有意思的场景:

interface Animal {
    name: string;
}
interface Bird {
    name: string;
    fly: boolean
}

let bird: Bird = { name: 'bbb', fly: true };
let animal: Animal = bird;  // 正确

这里却正确了。通过这个例子我们可以了解到,如果赋值都左右2侧都是 已经确定类型都内容,ts其实不是做额外属性检查,而是判断右侧都类型是否左侧都子类型,是否能满足左侧都类型。

本文总结

本篇文章内容还不少,主要是让大家先对可以定义自己类型有个基础的认识,那我们后续可以继续展开一些相对高级的用法。