likes
comments
collection
share

细说 TypeScript 的可索引类型和类类型

作者站长头像
站长
· 阅读数 24

用代码行数来评估程序的开发进度,就好比是拿重量来评估一个飞机的建造进度。


最近在查阅 TypeScript 文档时偶然看到了这两个概念,最后的示例给我整懵了没明白为什么要这么做,下面是对示例做的解读。

Indexable Types 可索引类型

TS3 文档 TS4 文档

与使用接口描述属性和方法,接口也可以描述通过索引获取的元素或属性,如 arr[0]、obj['name']。可索引类型有一个索引别名(key),它描述了用来索引元素或属性的类型,以及相应的索引返回值(value)类型。

// 用索引获取数组元素
interface StudentArray {
  [index: number]: string;
}
let studentArr: StudentArray = ["Bob", "Fred"];
let student1: string = studentArr[0]; // 'Bob'
let student2: string = studentArr[1]; // 'Fred'

// 用索引获取对象属性
interface StudentObject {
  [key: string]: number;
}
let studentObj: StudentObject = {};
studentObj['Bob'] = 1;
studentObj['Fred'] = 2; // { Bob: 1, Fred: 2 }
💡 TS 支持 string 和 number 索引类型,可以同时使用两种类型的索引,但 number 索引的值(value)类型必须是 string 索引值(value)类型的子类型。

这是因为 JavaScript 语言规定:对象的键名一律为字符串。使用索引获取对象时,非字符串索引会被自动转为字符串。因为数组本身是对象,所以数组的索引(下标)实际上是字符串。(参见:ECMA5.1 属性访问器

interface Animal {
  type: string;
}

interface Dog extends Animal {
  name: string;
}

interface DogData {
  [x: number]: Dog;
  [x: string]: Animal;
}

let hotDog: DogData = {};
let animal: Animal = { type: 'dog' };
let dog: Dog = { type: 'dog', name: 'hotDog' };
hotDog['0'] = animal;
hotDog[0] = dog;
// error TS2741: Property 'name' is missing in type 'Animal' but required in type 'Dog'

不过就算 number 索引的值为子类型,两种索引依然不能重名,否则运行时会报错,除非子类型的数据结构和父类型是一致的。

class type 类类型

TS3 文档 (TS4 文档中已弃用)

实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去实现某个接口。

// 定义 ClockInterface 接口,里面包含 currentTime 属性
interface ClockInterface {
    currentTime: Date;
}

// 由 Clock 类去实现 ClockInterface 接口,并创建构造函数
class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

或者在接口中描述一个 setTime 方法,在类里实现它。

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

接口只描述类的公共部分,公共部分由类自行定义

类静态部分与实例部分

类有两个类型:静态部分的类型(constructor)和实例部分的类型(属性、方法)。如果想要创建一个类,去实现带有构造函数的接口会报错

interface ClockConstructor {
  // 描述构造函数
  new (hour: number, minute: number);
}

// 类“Clock”错误实现接口“ClockConstructor”。
// 类型“Clock”提供的内容与签名“new (hour: number, minute: number): any”不匹配。
class Clock implements ClockConstructor {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

会报错是因为类实现接口时,TS 只会对实例部分(属性、方法)做类型检查,而 constructor 属于类的静态部分,不在 TS 的检查范围内。

想要实现对类静态部分的类型检查,需要直接处理类的静态部分。首先定义两个接口,一个是 ClockConstructor 用于描述构造函数,一个是 ClockInterface 用于描述实例方法和属性。其次再定义一个函数 createClock ,在函数内创建实例并返回。

💡 最开始可以不使用 ClockInterface
interface ClockConstructor {
  // 描述构造函数的形参类型和返回类型
  new (hour: number, minute: number);
}

// 传入 ClockConstructor 类型和 hour、minute 参数
function createClock(ctor: ClockConstructor,  hour: number,  minute: number) {
  // new 一个 ClockConstructor 类型实例
  // 传入构造函数参数 hour 和 minute
  return new ctor(hour, minute);
}

这样是可行的,修改 createClock 的 hour、minute 类型会报错,但会因为无法实现 ClockConstructor 接口导致 createClock 方法不起作用。

💡 现在加上 `ClockInterface`
interface ClockConstructor {
  // 描述构造函数的形参类型和返回类型
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  showTime(): void;
}

// 传入 ClockConstructor 类型和 hour、minute 参数,返回 ClockInterface 类型
function createClock(ctor: ClockConstructor,  hour: number,  minute: number): ClockInterface {
  // new 一个 ClockConstructor 类型实例
  // 传入构造函数参数 hour 和 minute
  // ClockConstructor 的 构造函数会返回 ClockInterface 类型
  return new ctor(hour, minute);
}

这一段代码其实就是模拟接口定义,createClockClockConstructorClockInterface 两个接口结合了起来。

💡 最后把这个模拟接口运用起来
interface ClockConstructor {
  // 描述构造函数的形参类型和返回类型
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  showTime(): void;
}

// 传入 ClockConstructor 类型和 hour、minute 参数,返回 ClockInterface 类型
function createClock(ctor: ClockConstructor,  hour: number,  minute: number): ClockInterface {
  // new 一个 ClockConstructor 类型实例
  // 传入构造函数参数 hour 和 minute
  // ClockConstructor 的 构造函数会返回 ClockInterface 类型
  return new ctor(hour, minute);
}

// 因为 ClockConstructor 的构造函数需要返回 ClockInterface 类型,
// 所以 ClockImpl 的数据结构要同时包括 ClockConstructor 和 ClockInterface 的构造函数、属性和方法
class ClockImpl implements ClockInterface {
  hour: number;
  minute: number;
  constructor(h: number, m: number) {
    this.hour = h;
    this.minute = m;
  }
  showTime() {
    console.log(`Time now: ${this.hour}:${this.minute}`);
  }
}

// 创建 clock 实例
let clock = createClock(ClockImpl, 12, 17);
clock.showTime();

因为 createClock 的第一个参数是 ClockConstructor 类型,在 createClock(ClockImpl, 12, 17) 里,会检查 ClockImpl 是否符合构造函数。

最后要明白,这样写的目的就是为了能在接口里描述构造函数并实现。

参见 GitHub 的讨论:Constructor in interfaces throws error

以及 Stack Overflow:How does interfaces with construct signatures work?

💡 另一种使用类表达式的简单写法
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  showTime(): void;
}

// Clock 类的类型为 ClockConstructor,并从 ClockInterface 接口实现,所以数据结构要包括两者
const Clock: ClockConstructor = class Clock implements ClockInterface {
  hour: number;
  minute: number;
  constructor(h: number, m: number) {
    this.hour = h;
    this.minute = m;
  }
  showTime() {
    console.log(`Time now: ${this.hour}:${this.minute}`);
  }
};

let clock = new Clock(12, 17);
clock.showTime();
转载自:https://juejin.cn/post/7041012630198157319
评论
请登录