TypeScript学习拾遗——enum枚举和class的“值类二象性”

597 阅读4分钟

前言

区分“值”和“类型”是理解TS类型系统的关键要素之一,在大多数情况下,“值”和“类型”是不能混同的,例如let A = 0type B = 0,前者是“值”,后者是“类型”,我们不能把值当做类型操作,也不能把类型作为值使用:

let A = 0
let a: A = 0 // 语法错误,A是值而不是类型

type B = 0
let c = B // 语法错误,B是类型而不是值

哪怕是刚入门TS的人,也不会犯这么低级的错误,但是在TS中,有两个特殊的存在:classenum,它们既是值,也是类型,有着诸多迷惑人的地方,我愿称之为“值类二象性”。

class

关于class的值类二象性,我曾经在TS类型体操(三) TS内置工具类2#类的“值类二象性”有过讨论,这里我直接说结论:

class作为一个值的时候,它是一个构造器(或者叫构造函数),作为一个类型时,它是自身作为构造器的实例类型

例如class Person

class Person {}

Person的值和类型,其关系如下图所示:

类构造器实例图示.jpg

代码示意:

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

这里的PersonTypePersonInstance、以及作为类型的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 // √ 没问题