前言
区分“值”和“类型”是理解TS类型系统的关键要素之一,在大多数情况下,“值”和“类型”是不能混同的,例如let A = 0和type B = 0,前者是“值”,后者是“类型”,我们不能把值当做类型操作,也不能把类型作为值使用:
let A = 0
let a: A = 0 // 语法错误,A是值而不是类型
type B = 0
let c = B // 语法错误,B是类型而不是值
哪怕是刚入门TS的人,也不会犯这么低级的错误,但是在TS中,有两个特殊的存在:class和enum,它们既是值,也是类型,有着诸多迷惑人的地方,我愿称之为“值类二象性”。
class
关于class的值类二象性,我曾经在TS类型体操(三) TS内置工具类2#类的“值类二象性”有过讨论,这里我直接说结论:
class作为一个值的时候,它是一个构造器(或者叫构造函数),作为一个类型时,它是自身作为构造器的实例类型。
例如class Person
class Person {}
Person的值和类型,其关系如下图所示:
代码示意:
class Person {}
let person = new Person // 作为一个值,Person是一个构造器
type PersonType = typeof person // person是Person的实例,PersonType自然是Person的实例类型。
type PersonConstructor = typeof Person // Person作为值时是构造器,PersonConstructor是Person的构造器类型。
type PersonInstance = InstanceType<PersonConstructor> // PersonInstance也是Person的实例类型。
// 测试三者的关系
type t1 = PersonType extends PersonInstance ? true : false
type t2 = PersonInstance extends PersonType ? true : false
type t3 = PersonInstance extends Person ? true : false
type t4 = Person extends PersonInstance ? true : false
type t5 = Person extends PersonType ? true : false
type t6 = PersonType extends Person ? true : false
这里的PersonType、PersonInstance、以及作为类型的Person,三者是同一个东西,以上的t1到t6,结果全都是true。
enum
enum枚举也具有“值类二象性”,例如,枚举AB:
enum AB {
A,
B,
}
编译成JS之后是这样的:
"use strict";
var AB;
(function (AB) {
AB[AB["A"] = 0] = "A";
AB[AB["B"] = 1] = "B";
})(AB || (AB = {}));
虽然看起来有点复杂,但其实就等价于:
var AB = {
A: 0,
B: 1,
}
所以,TS声明的枚举,在运行时中相当于声明了一个对象,以枚举的每一项当键名,键值默认是从0开始递增的数字,AB作为一个值,其类型是typeof AB:
enum AB {
A = 'a',
B = 'b',
}
const ab: typeof AB = AB // 这里的两个AB都是值,typeof将值转换为类型
但是另一方面,枚举又是一个类型:
enum AB {
A,
B,
}
const a: AB = 0
const b: AB = 1
如果你接触过其他语言的枚举,应该不难理解这种用法,这里的AB作为类型,就相当于联合类型0 | 1。
不仅枚举本身具有“值类二象性”,枚举的每个成员,也都具有“值类二象性”:
enum AB {
A = 'a',
B = 'b',
}
const a: AB.A = AB.A // AB.A,前者是类型,后者是值
const b: AB.B = AB.B// AB.B,前者是类型,后者是值
需要注意的是,当枚举的是字符串时,我们将不能直接用字符串字面量给其赋值,这是枚举的一个奇异特性。
枚举的奇异特性
上面谈到,AB枚举数字时,它作为类型相当于联合类型0 | 1,我们可以先验证一下:
enum AB {
A,
B,
}
type T = 0 | 1
type t1 = AB extends T ? true : false
type t2 = T extends AB ? true : false
t1和t2结果都是true,没毛病。
但是,如果枚举的是字符串:
enum AB {
A = 'a',
B = 'b',
}
type T = 'a' | 'b'
type t1 = AB extends T ? true : false
type t2 = T extends AB ? true : false
t1的结果是true,但t2的结果却是false!也就是说,AB是'a' | 'b'的子类型,但不等于'a' | 'b'。
为了彻底搞清楚为什么,我再单独测试一下AB.A:
enum AB {
A = 'a',
B = 'b'
}
type t3 = AB.A extends 'a' ? true : false
type t4 = 'a' extends AB.A ? true : false
这次倒是不意外:t3的结果是true,t4结果是是false:AB.A是'a'的子类型,但不等于'a'。
由此便解释了我上面提到的现象:如果枚举的是字符串,我们将不能直接用字符串的字面量给其赋值。
enum AB {
A = 'a',
B = 'b'
}
const a: AB = 'a' // 报错:不能将类型“"a"”分配给类型“AB”。
const b: AB.B = 'b' // 报错:不能将类型“"b"”分配给类型“AB.B”。
原理其实很简单:在TS中,父类型的值不能赋值给子类型的值,但反过来可以:
enum AB {
A = 'a',
B = 'b'
}
const a: 'a' = AB.A // √ 没问题
const b: string = AB.B // √ 没问题