1. 首页
  2. JS

TypeScript 中的 symbol 类型

简介

Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等。

Symbol 值通过Symbol()函数生成。在 TypeScript 里面,Symbol 的类型使用symbol表示。

let x: symbol = Symbol();
let y: symbol = Symbol();

x === y; // false

上面示例中,变量xy的类型都是symbol,且都用Symbol()生成,但是它们是不相等的。

unique symbol

symbol类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值。

比如,5是一个具体的数值,就用5这个字面量来表示,这也是它的值类型。但是,Symbol 值不存在字面量,必须通过变量来引用,所以写不出只包含单个 Symbol 值的那种值类型。

为了解决这个问题,TypeScript 设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。

因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明。

// 正确
const x: unique symbol = Symbol();

// 报错
let y: unique symbol = Symbol();

上面示例中,let命令声明的变量,不能是unique symbol类型,会报错。

const命令为变量赋值 Symbol 值时,变量类型默认就是unique symbol,所以类型可以省略不写。

const x: unique symbol = Symbol();
// 等同于
const x = Symbol();

每个声明为unique symbol类型的变量,它们的值都是不一样的,其实属于两个值类型。

const a: unique symbol = Symbol();
const b: unique symbol = Symbol();

a === b; // 报错

上面示例中,变量a和变量b的类型虽然都是unique symbol,但其实是两个值类型。不同类型的值肯定是不相等的,所以最后一行就报错了。

由于 Symbol 类似于字符串,可以参考下面的例子来理解。

const a: "hello" = "hello";
const b: "world" = "world";

a === b; // 报错

上面示例中,变量ab都是字符串,但是属于不同的值类型,不能使用严格相等运算符进行比较。

而且,由于变量ab是两个类型,就不能把一个赋值给另一个。

const a: unique symbol = Symbol();
const b: unique symbol = a; // 报错

上面示例中,变量a和变量b的类型都是unique symbol,但是其实类型不同,所以把a赋值给b会报错。

上例变量b的类型,如果要写成与变量a同一个unique symbol值类型,只能写成类型为typeof a

const a: unique symbol = Symbol();
const b: typeof a = a; // 正确

不过我们知道,相同参数的Symbol.for()方法会返回相同的 Symbol 值。TypeScript 目前无法识别这种情况,所以可能出现多个 unique symbol 类型的变量,等于同一个 Symbol 值的情况。

const a: unique symbol = Symbol.for("foo");
const b: unique symbol = Symbol.for("foo");

console.log(a === b); // 浏览器会报错:此比较似乎是无意的,因为类型“typeof a”和“typeof b”没有重叠。ts(2367)

上面示例中,变量ab是两个不同的值类型,但是它们的值其实是相等的。

unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。

const a: unique symbol = Symbol();

const b: symbol = a; // 正确

const c: unique symbol = b; // 报错

上面示例中,unique symbol 类型(变量a)赋值给 symbol 类型(变量b)是可以的,但是 symbol 类型(变量b)赋值给 unique symbol 类型(变量c)会报错。

unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol。

const x: unique symbol = Symbol();
const y: symbol = Symbol();

interface Foo {
  [x]: string; // 正确
  [y]: string; // 报错
}

上面示例中,变量y当作属性名,但是y的类型是 symbol,不是固定不变的值,导致报错。

unique symbol类型也可以用作类(class)的属性值,但只能赋值给类的readonly static属性。

class C {
  static readonly foo: unique symbol = Symbol();
}

上面示例中,静态只读属性foo的类型就是unique symbol。注意,这时staticreadonly两个限定符缺一不可,这是为了保证这个属性是固定不变的。

类型推断

如果变量声明时没有给出类型,TypeScript 会推断某个 Symbol 值变量的类型。

let命令声明的变量,推断类型为 symbol。

// 类型为 symbol
let x = Symbol();

const命令声明的变量,推断类型为 unique symbol。

// 类型为 unique symbol
const x = Symbol();

但是,const命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。

let x = Symbol();

// 类型为 symbol
const y = x;

let命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。

const x = Symbol();

// 类型为 symbol
let y = x;

Symbol 的主要使用场景

Symbol的主要特点是唯一性,这使得它在很多场景下非常有用。

1. 对象属性的唯一键

防止属性名冲突,特别适合在扩展对象时使用:

const logLevel = {
    DEBUG: Symbol('DEBUG'),
    INFO: Symbol('INFO'),
    WARN: Symbol('WARN'),
    ERROR: Symbol('ERROR')
};

function log(level, message) {
    if (level === logLevel.DEBUG) {
        console.debug(message);
    } else if (level === logLevel.ERROR) {
        console.error(message);
    }
}

2. 模拟私有属性

虽然不是真正的私有,但可以避免意外访问:

const _internalData = Symbol('internalData');

class MyClass {
    constructor() {
        this[_internalData] = '私有数据';
    }
    
    getData() {
        return this[_internalData];
    }
}

const instance = new MyClass();
console.log(instance[_internalData]); // 可以访问,但需要 Symbol 引用

3. 内置 Symbol(Well-known Symbols)

用于自定义对象的内置行为:

// 自定义迭代器
class MyIterable {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}

// 自定义类型转换
class Temperature {
    constructor(celsius) {
        this.celsius = celsius;
    }
    
    [Symbol.toPrimitive](hint) {
        if (hint === 'string') return `${this.celsius}°C`;
        return this.celsius;
    }
}

4. 注册全局单例 Symbol

使用 Symbol.for()创建全局可重用的 Symbol:

// 在不同的模块中共享同一个 Symbol
// module1.js
const MY_APP = Symbol.for('myapp.config');

// module2.js
const configKey = Symbol.for('myapp.config');
// 获取到同一个 Symbol

5. 避免魔法字符串

用 Symbol 代替容易出错的字符串常量:

// 不好的做法
const STATUS = {
    PENDING: 'PENDING',
    SUCCESS: 'SUCCESS',
    ERROR: 'ERROR'
};

// 更好的做法
const STATUS = {
    PENDING: Symbol('PENDING'),
    SUCCESS: Symbol('SUCCESS'),
    ERROR: Symbol('ERROR')
};

6. React 中的使用场景

// 自定义 React 上下文
const ThemeContext = Symbol('ThemeContext');
const UserContext = Symbol('UserContext');

// 在组件中安全地存储元数据
const IS_SUSPENSE = Symbol('isSuspense');

7. 枚举的替代方案

// 比字符串枚举更安全
const Direction = {
    UP: Symbol('UP'),
    DOWN: Symbol('DOWN'),
    LEFT: Symbol('LEFT'),
    RIGHT: Symbol('RIGHT')
};

function move(direction) {
    switch (direction) {
        case Direction.UP: // 不会与其他值冲突
        case Direction.DOWN:
            // ...
    }
}

8. 元编程和框架开发

// 框架内部使用的元数据
const METADATA = Symbol('metadata');

function decorator(target) {
    target[METADATA] = { version: '1.0' };
}

9.注意事项

不可枚举性:Symbol 属性默认不会出现在 for...inObject.keys()

需要显式获取Object.getOwnPropertySymbols()可以获取对象的 Symbol 属性

JSON 序列化:Symbol 属性不会被 JSON.stringify()包含

Symbol 的核心价值在于提供唯一且不会冲突的标识符,特别适合库开发、框架设计和需要避免命名冲突的场景。


TOP