TypeScript入门与实战
上QQ阅读APP看书,第一时间看更新

5.3.5 symbol与unique symbol

TypeScript中的symbol类型对应于JavaScript中的Symbol原始类型。该类型能够表示任意的Symbol值。

symbol类型使用symbol关键字来表示。示例如下:


01 // 自定义Symbol
02 const key: symbol = Symbol();
03 
04 // Well-Known Symbol
05 const symbolHasInstance: symbol = Symbol.hasInstance;

在3.4节中介绍过,字面量能够表示一个固定值。例如,数字字面量“3”表示固定数值“3”;字符串字面量“'up'”表示固定字符串“'up'”。symbol类型不同于其他原始类型,它不存在字面量形式。symbol类型的值只能通过“Symbol()”和“Symbol.for()”函数来创建或直接引用某个“Well-Known Symbol”值。示例如下:


01 const s0: symbol = Symbol();
02 const s1: symbol = Symbol.for('foo');
03 const s2: symbol = Symbol.hasInstance;
04 const s3: symbol = s0;

为了能够将一个Symbol值视作表示固定值的字面量,TypeScript引入了“unique symbol”类型。“unique symbol”类型使用“unique symbol”关键字来表示。示例如下:


01 const s0: unique symbol = Symbol();
02 const s1: unique symbol = Symbol.for('s1');

“unique symbol”类型的主要用途是用作接口、类等类型中的可计算属性名。因为如果使用可计算属性名在接口中添加了一个类型成员,那么必须保证该类型成员的名字是固定的,否则接口定义将失去意义。下例中,允许将“unique symbol”类型的常量x作为接口的类型成员,而symbol类型的常量y不能作为接口的类型成员,因为symbol类型不止包含一个可能值:


01 const x: unique symbol = Symbol();
02 const y: symbol = Symbol();
03 
04 interface Foo {
05     [x]: string; // 正确
06 
07     [y]: string;
08 //  ~~~
09 //  错误:接口中的计算属性名称必须引用类型为字面量类型
10 //  或'unique symbol'的表达式
11 }

我们将在后面的章节中介绍类和接口类型。

实际上,“unique symbol”类型的设计初衷是作为一种变通方法,让一个Symbol值具有字面量的性质,即仅表示一个固定的值。“unique symbol”类型没有改变Symbol值没有字面量表示形式的事实。为了能够将某个Symbol值视作表示固定值的字面量,TypeScript对“unique symbol”类型和Symbol值的使用施加了限制。

TypeScript选择将一个Symbol值与声明它的标识符绑定在一起,并通过绑定了该Symbol值的标识符来表示“Symbol字面量”。这种设计的前提是要确保Symbol值与标识符之间的绑定关系是不可变的。因此,TypeScript中只允许使用const声明或readonly属性声明来定义“unique symbol”类型的值。示例如下:


01 // 必须使用const声明
02 const a: unique symbol = Symbol();
03 
04 interface WithUniqueSymbol {
05     // 必须使用readonly修饰符
06     readonly b: unique symbol;
07 }
08 
09 class C {
10     // 必须使用static和readonly修饰符
11     static readonly c: unique symbol = Symbol();
12 }

此例第1行,常量a的初始值为Symbol值,其类型为“unique symbol”类型。在标识符a与其初始值Symbol值之间形成了绑定关系,并且该关系是不可变的。这是因为常量的值是固定的,不允许再被赋予其他值。标识符a能够固定表示该Symbol值,标识符a的角色相当于该Symbol值的字面量形式。

如果使用let或var声明定义“unique symbol”类型的变量,那么将产生错误,因为标识符与Symbol值之间的绑定是可变的。示例如下:


01 let a: unique symbol = Symbol();
02 //  ~
03 //  错误:'unique symbol' 类型的变量必须使用'const'
04 
05 var b: unique symbol = Symbol();
06 //  ~
07 //  错误:'unique symbol' 类型的变量必须使用'const'

“unique symbol”类型的值只允许使用“Symbol()”函数或“Symbol.for()”方法的返回值进行初始化,因为只有这样才能够“确保”引用了唯一的Symbol值。示例如下:


01 const a: unique symbol = Symbol();
02 const b: unique symbol = Symbol('desc');
03 
04 const c: unique symbol = a;
05 //    ~
06 //    错误:a的类型与c的类型不兼容
07 
08 const d: unique symbol = b;
09 //    ~
10 //    错误:b的类型与d的类型不兼容

但是,我们知道使用相同的参数调用“Symbol.for()”方法实际上返回的是相同的Symbol值。因此,可能出现多个“unique symbol”类型的值实际上是同一个Symbol值的情况。由于设计上的局限性,TypeScript目前无法识别出这种情况,因此不会产生编译错误,开发者必须要留意这种特殊情况。示例如下:


01 const a: unique symbol = Symbol.for('same');
02 const b: unique symbol = Symbol.for('same'); 

此例中,编译器会认为a和b是两个不同的Symbol值,而实际上两者是相同的。

在设计上,每一个“unique symbol”类型都是一种独立的类型。在不同的“unique symbol”类型之间不允许相互赋值;在比较两个“unique symbol”类型的值时,也将永远返回false。示例如下:


01 const a: unique symbol = Symbol();
02 const b: unique symbol = Symbol();
03 
04 if (a === b) {
05 //  ~~~~~~~
06 //  该条件永远为false
07 
08     console.log('unreachable code');
09 }

由于“unique symbol”类型是 symbol类型的子类型,因此可以将“unique symbol”类型的值赋值给symbol类型。示例如下:


01 const a: unique symbol = Symbol();
02 
03 const b: symbol = a;

如果程序中未使用类型注解来明确定义是symbol类型还是“unique symbol”类型,那么TypeScript会自动地推断类型。示例如下:


01 // a和b均为'symbol'类型,因为没有使用const声明
02 let a = Symbol();
03 let b = Symbol.for('');
04 
05 // c和d均为'unique symbol'类型
06 const c = Symbol();
07 const d = Symbol.for('');
08 
09 // e和f均为'symbol'类型,没有使用Symbol()或Symbol.for()初始化
10 const e = a;
11 const f = a;

关于类型推断的详细介绍请参考7.3节。