我觉得无聊,于是把 Rust 枚举带到了 TypeScript 里——一段让人怀疑人生的选择故事
嘿,各位代码爱好者!围过来听我讲一个故事给大家听。故事是这样的:我心血来潮,在一种高效拖延的状态下,决定在TypeScript中实现类似Rust的枚举类型。你可能会问,为什么呢?好吧,为什么不呢?反正我没有其他更好的事情可做,比如学着织毛衣或者钩针织品,或者整理一下我的袜子。
一个值得怀疑的想法的起源:想象一下:凌晨两点,周二晚上(或者周三早上,如果你是那种“半杯满”的人)。我穿着睡衣,周围是空的咖啡杯和一堆让人觉得有点奇怪的橡皮鸭。我刚一口气看了一个关于铁锈的系列节目……你知道,就是金属氧化的过程。真的挺有意思的。
突然,因咖啡因而亢奋的大脑产生了一个联想:Rust……Rust编程语言……TypeScript……啊哈!为什么不把Rust中独特且迷人的枚举系统带入TypeScript呢?这种想法只有在你极度疲劳并且极度自信时听起来才合理(或者像我这样使用tauri构建应用时) 😬😅。
但是为什么不行呢?问题不是“为什么?”而是“为什么不?”毕竟,最坏会怎样?(旁白:其实会有不少,但还是别扫兴好了。)
-
因为我能:有时候,做某件事最好的理由就是你能够去做。这就是为什么人们会去爬山、吃魔鬼椒或用Ruby写代码的逻辑。
-
为了炫耀:想想你得意的样子!你做的待办事项应用真是可爱。“哦,你实现了一个待办事项应用?真不错。我可是从一种语言到另一种语言完整地移植了一个枚举。这根本不算什么。”
-
至少我这么告诉自己,来为即将来临的睡眠不足找借口。
- 因为我没工作:还有比这更明显的冒牌特征吗?在一种编译成某种类似东西的伪类型玩具语言中实现一个强类型数据结构。
想来想去,再回头已经不可能了。
启程了!
只带着决心,对 Rust 的模糊理解,以及相当于一个小型城市所需的咖啡因,我出发了。以下是我捣鼓出来的一小部分成果。
const TAG = Symbol("__tag__");
const UNION = Symbol("__union__");
export type Variant<T extends string, V = {}, U = {}> = {
[TAG]: T;
[UNION]: U;
} & V;
// ... (更多当时在凌晨三点看起来有意义的类型定义)
export const choice = <T extends Record<string, any>>(def: T) => {
// 当时觉得不错的实现细节
};
进入全屏模式 退出模式
我知道……我几乎什么都没给你展示过……但在打这些字时,我不禁觉得自己就像弗兰肯斯坦博士(也就是那位著名的科学家),把不同语言的部分缝合在一起,试图创造一个可能想要吃掉我的脑子的东西(是的,我有两个大脑……而且它们并不怎么合得来)。
而且说实话,我本打算在这篇文章中逐节讲解这个实现,逐一讲解每个细节……但说真的,没人会读这篇文章,使用这个实现的人可能更少(大概也不应该这样做)。所以,我直接把整个内容放下面,然后谈谈我们学到的一些心得。
我想要的 vs. 我得到的所以,这就是我搞到的。
const TAG = Symbol("__tag__");
const UNION = Symbol("__union__");
// Struct 类型会自动包含 'type' 属性
export type Variant<T extends string, V = {}, U = {}> = {
[TAG]: T;
[UNION]: U;
} & V;
export namespace Variant {
export type Tag<T extends Variant<any>> = `${T[typeof TAG]}`;
}
export type Variants<T> = {
[K in keyof T]: Variant<K & string, T[K], Union<T>>;
};
export namespace Variants {
// 导出类型 Of<T extends Union<any>>
export type Of<T extends Union<any>> = { [K in keyof T]: ReturnType<T[K]> };
// 导出类型 From<T extends Variant<any>>
export type From<T extends Variant<any>> = Variants.Of<T[typeof UNION]> & {
[Key in Variant.Tag<T>]: T;
};
}
export type Union<T> = {
[K in keyof T]: <U extends T[K]>(
value: U
) => Variant<K & string, U, Union<T>>;
};
export namespace Union {
export type Of<T extends Variant<any> | Variants<any>> = T extends {
[UNION]: infer U;
}
? U
: T extends Variants<any>
? T[keyof T][typeof UNION]
: never;
}
export type Choice<T> = Variants<Readonly<T>>[keyof T];
export namespace Choice {
export type Of<T extends Variants.Of<any> | Union<any>> =
T extends Union<any> ? Variants.Of<T>[keyof T] : T[keyof T];
}
type CompleteMatchPattern<T, R> = {
[K in keyof T]: (value: T[K]) => R;
};
type PartialMatchPattern<T, R> = {
[K in keyof T]?: (value: T[K]) => R;
} & { _: () => R };
export type MatchPattern<T, R> =
| CompleteMatchPattern<T, R>
| PartialMatchPattern<T, R>;
export const choice = <T extends Record<string, any>>(def: T) => {
let Union: any = {};
let struct: any = {};
for (let [key, defaultValue] of Object.entries(def)) {
struct[key] = (value: object) =>
Object.assign(Object.create(Union), {
...defaultValue,
...value,
[TAG]: key,
[UNION]: struct
});
}
return struct as Union<T>;
};
export function match<
T extends Variant<any>,
P extends MatchPattern<Variants.From<T>, R>,
R
>(
value: T,
patterns: P
): P extends MatchPattern<Variants.From<T>, infer R> ? R : R {
const pattern = patterns[value[TAG] as keyof T[typeof UNION]];
if (!pattern) {
throw new Error(`未处理的变体: ${value[TAG]},请检查并处理`);
}
return pattern(value as any) as P extends MatchPattern<
Variants.From<T>,
infer R
>
? R
: R;
}
点击全屏模式,点击退出全屏
尽管我尽量让 TypeScript 的语法看起来像 Rust,但这根本不可能,因为 TypeScript 和 Rust 不一样。所以这段代码虽然看起来不错:
//example.rs
枚举 Sandwich {
汉堡包 { with: Vec<Ingredient>, },
大堡 { with: Vec<Ingredient>, size: i8, },
Hotdog { with: Vec<Condiment>, }
}
点击全屏,点击退出全屏
不过,Typescript 不认为热狗(Hot Dog)是夹肉面包,并且已经占用了 enum
关键字,实现起来就像从没真正需要选择过一样。所以我将我的枚举称为……一个“选择”。
因为_TYPE _中的类型是抽象的,定义我的choice
类型时也需要一些特殊的语法:
/*
//example.ts
type 三明治 = 类型的选择.<typeof 三明治>
const 三明治 = 选择({
单手汉堡: { 包含: 配料列表 },
英雄堡: { 包含: 配料列表, 大小: 数字大小 },
热狗堡: { 包含: 配料列表 }
})
切换到全屏模式或退出全屏
嗯……你的眼睛没有看错。为了得到我习惯了的来自 Rust 枚举的使用体验,我不得不实现了一个 trick,用类型定义覆盖 choice 常量。这样做只是为了让你能够对类型 Sandwich
进行模式匹配,并使 TypeScript 能理解如何验证其分支。(关于匹配的内容,以后再说吧……我现在有点累了,所以可能就不会说得太长了。)
经过了感觉像几年但实际上可能只是几个小时的编码和嘟囔之后,我有了一个可用的实现。为了测试它,我决定建模一个对我来说很重要的项目:一个用于分类我越来越多的与编码相关的理由的系统。
import { Choice, choice, match } from "./enum";
import { number, string } from "./types";
type 借口 = Choice.Of<typeof 借口>;
const 借口 = choice({
咖啡短缺: { 需要的杯数: number },
计算机问题: { 错误信息: string },
灵感: { 等待的灵感来源: string },
});
function 解释延迟(借口: 借口): string {
return match(借口, {
咖啡短缺: ({ 需要的杯数 }) =>
`我还需要再喝 ${需要的杯数} 杯咖啡才能开始工作。`,
计算机问题: ({ 错误信息 }) =>
`我的电脑显示“${错误信息}”。我也是一头雾水。`,
灵感: ({ 等待的灵感来源 }) =>
`我在等待 ${等待的灵感来源} 给我灵感。随时都会有灵感来临。`,
});
}
const 我的借口 = 借口.灵感({ 等待的灵感来源: "编码之神" });
console.log(解释延迟(我的借口));
// 输出: 我在等待编码之神给我灵感。马上就会有灵感来了。
进入全屏,退出全屏
我不得不在 TypeScript 中对原始类型进行封装,以便让它们看起来更自然。当我运行这段代码并看到它工作时,我感到既兴奋又害怕。我成功了,但这样做的代价是什么?
《除了质疑我的生活选择之外的代价》-
时间就是金钱:由于整个过程使用了实时检查,你得花钱才能用这个东西。但大部分成本都花在构建上了。
-
空间被浪费:所以谁在乎这可能要花多少钱。我一直以为空间应该是无限的
- 脆弱,这就是Typescript:这只是一个临时解决办法……但说实话,但玩起来还挺有意思的。
那么,除了我了解到的 TypeScript 可能出现的每一个错误信息,这次经历除了给我带来黑眼圈,我还获得了什么呢?
-
更深入理解类型系统,以及对以此为生的语言设计者的新敬意。
-
提升解决问题的能力:如果你能搞定跨语言的功能,你大概就能找到世界和平的办法,对不对?大概吧?
- 一个适合在派对上讲的伟大故事:因为没有什么像谈论枚举的实现一样能说“派对的灵魂”,对吧?
Note: The phrase "life of the party" is a colloquial expression meaning the person who is the center of attention at a party, often translated more naturally in Chinese as "派对的灵魂" (the soul of the party) or "派对的灵魂人物" (the soul figure of the party).
结论是:这样做值得吗?坐在这里,品尝我的第n杯咖啡,盯着我的作品,我在想:这样做值得我花这么多时间吗?
绝对不行。而且我愿意再这么做一次。
有时候,亲爱的朋友,旅程比目的地还要重要。而有的时候,你只是需要做一些纯粹为了展示能力的复杂事情,仅仅为了证明你能行。
所以下次当你觉得无聊,想做一些有意义的事情时,记得我的故事。你可以学习一项新技能,参与开源项目,或者也许,试试看,你可以开始你自己的编程之旅。
再说,干嘛不说?
附注: 这个事情其实让我觉得有一种更严格且可能在TypeScript中非常有用的Result类型会更好。如果有人对此感兴趣,也许我会在另一晚不睡的时候再试试看。
附附注: 我知道我可能在这篇文章中漏掉了一些实现细节……为此我感到抱歉,但现在已经是早上7点了,我熬了一整夜,实在是力不从心……不过如果您有任何问题,欢迎随时在评论区留言。
我在codesandbox上放了一些代码样例,大家可以试着玩玩,随意fork一下。
代码沙盒 (在线代码编辑器)
共同学习,写下你的评论
评论加载中...
作者其他优质文章