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节。