细说 TypeScript 的可索引类型和类类型
用代码行数来评估程序的开发进度,就好比是拿重量来评估一个飞机的建造进度。
最近在查阅 TypeScript 文档时偶然看到了这两个概念,最后的示例给我整懵了没明白为什么要这么做,下面是对示例做的解读。
Indexable Types 可索引类型
与使用接口描述属性和方法,接口也可以描述通过索引获取的元素或属性,如 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
,在函数内创建实例并返回。
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
方法不起作用。
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);
}
这一段代码其实就是模拟接口定义,createClock
把 ClockConstructor
和 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);
}
// 因为 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