TypeScript: enum还是literal type?答案也许不那么显而易见

2023年 10月 9日 64.7k 0

引子

在使用Type Script + NodeJs作为技术栈开发一个后端service过程中(其实重头戏是fp-ts,但它和今天的主题无关),遇到一个有些有趣的小问题,遂记录如下。

需求很简单:用ts的type字段去定义一个domain modal(建模);后续在json序列化以后直接在endpoint里return出来。这可能在所有OOP的编程语言中都是第一步需要做的事(为了避免泄露业务代码,所有代码都被简化和代替了):

type Fruit = {
  fruitName: FruitName; // response里水果名字可能是apple,banana或orange的三选一,类型为string
  price: number;
};

const res: Fruit = {
  fruitName: "apple",  // 需求是在这里限定只能三种水果
  price: 3
};

...
response.status(200).json(res)
...

在构造fruitName的type(FruitName)的时候, 直接跳进大脑里的可能就是enumliteral type(毕竟显然水果名字是有限集里的枚举类型):

type Fruit = {
  fruitName: FruitName; 
  price: number;
};

// enum写法
enum FruitName {
  APPLE = "apple",
  BANANA = "banana",
  ORANGE = "orange"
}

// literal type写法
type FruitName = "apple" | "banana" | "orange"

当然在实际引用的时候,方法也有所不同:

// enum
const res: Fruit = {
  fruitName: FruitName.APPLE,
  price: 3
};

// literal type
const res: Fruit = {
  fruitName: "apple",  // 如果这里是"pineapple"的话,ts会编译失败(废话
  price: 3
};

很难说哪种引用方式更简洁,或者明晰。但在现代IDE的加持下,两种方式似乎都可以实现类型安全,以及编写代码实现时的良好提示体验。

那么是否enumliteral type没有任何区别呢?这种需求下是否并不存在best practice呢(我一度认为literal type更好。仅仅只是因为生产代码的枚举类型很多,这样可以少写几行代码,节省我的键盘耐久XD)?在我和同事们探索后,有了意想不到的发现。

编译差异

众所周知,TS仅仅在编译时生效,runtime的时候已经只剩pure的js代码了。那么,上述两种实现编译后的代码是否相同呢?

enum:

enum FruitName {
  APPLE = "apple",
  BANANA = "banana",
  ORANGE = "orange"
}

const a: FruitName = FruitName.APPLE

// 编译后
var FruitName;
(function (FruitName) {
    FruitName["APPLE"] = "apple";
    FruitName["BANANA"] = "banana";
    FruitName["ORANGE"] = "orange";
})(FruitName || (FruitName = {}));

const a = FruitName.APPLE;

literal type:

type FruitName = "apple" | "banana" | "orange"
const b: FruitName = "banana"

// 编译后

// ...是的,这里啥也没有
const b = "banana"  // 编译后,literal type的概念不复存在

不难看出,TypeScript编译器对于enum和literal type的实现机制是完全不同的:

enum会被编译成function + key-value的方式,实现通过key调用值(不给enum值的话,默认value是0,1,2的number);而literal type是一个纯粹runtime之前的概念。在编译之后,b将是一个纯粹的string

这个消息也许是利好literal type的。毕竟我们只需要在编译时保证类型安全,确保我们不会写出b = "pineapple"这样的代码。enum的编译后文件可读性显然差很多,而使用literal type可以让编译更简单,提高performance。

literal type的问题与解决方案

实际上,上面的literal type写法存在一个风险:如果我们有需求——在runtime列出所有可能的FruitName,此时enum由于会被编译成function与键值对,显然是可以实现的;但上述的literal type无法做到这一点,因为它被编译后只剩plain string。

如何解决呢?不卖关子(其实是节约键盘耐久),观察以下代码:

export const FruitName = ['apple', 'banana', 'orange'] as const; 
// FruitName: readonly ['apple', 'banana', 'orange']
type FruitName = typeof FruitName[number]; 
// type FruitName = "apple" | "banana" | "orange"

这种实现依赖于TS5.0支持的如下新特性,感兴趣可以在官网了解更多:

  • as const语法
  • type name可以与val name重名(编译器处理存在于不同空间)
  • typeof语法

由于const FruitName会被正常编译,所以可以通过锚定它,在使用literal type的同时遍历或者获取所有可能的枚举类型。

const enum

如果你实在无法割舍enum,譬如习惯使用形如FruitName.APPLE这样的调用方式(笔者个人认为这种调用确实可读性更强),但同时想让编译后的代码像literal type那样更无痕;实际上TS也提供了这样的feature,也就是const enum

注意下面的代码:

const enum FruitName {  // 唯一区别是enum前加const关键字
  APPLE = "apple",
  BANANA = "banana",
  ORANGE = "orange"
}

const c: FruitName = FruitName.ORANGE

// 编译后
// const enum本身不会被编译

const c = "orange" /* FruitName3.ORANGE */;  // 这里的注释是编译器添加的

好奇的话,你可以在这里试试ts和编译后的js代码,或许可以加深印象。

总结(太长不看)

最后,我在项目中使用了const enum,为了兼顾编译效率和调用便利。我和我的小伙伴还是习惯于Key.Value的调用模式。

当然,有些问题即使是const enum也无法规避,比如跨文件使用必须先import,在同一scope下重复声明同名enum不会报编译错误等等,所以我相信我们最后的选择也并不是标准答案。具体使用哪种方案,可能取决于你的团队习惯、代码风格、use case。

而且我这篇文章也很有可能在某个时间点过时。举个例子,假如有一天JS有了自己的enum,而微软因此对TS的enum做了某些breaking change的话……(暂时不太可能,截至发文ECMAScript的enum预案还处于stage 0)

真实世界的feature永远处于动态发展的过程中。工程师必须永远好奇,永远不满足于标准答案。

参考资料

  • TypeScript enums: How do they work? What can they be used for?
  • Difference between string enums and string literal types in TS
  • Optimize Enums in TypeScript with "const"
  • Proposal for ECMAScript enums
  • Alternatives to TypeScript enum
  • How to list the possible values of a string literal union type in TypeScript
  • TypeScript Playground

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论