关于 JavaScript 非单子 Promise 的更多陷阱

关于 JavaScript 非单子 Promise 的更多陷阱

💡 原文英文,约400词,阅读约需2分钟。
📝

内容提要

JavaScript的Promise不是一个单子。它会将Promise<Promise<T>>当作Promise<T>处理。这可能会导致意想不到的问题。解决方法是改变pop()的签名,使用Maybe<T>来代替Promise<T | undefined>。这样可以避免T包含Promise<undefined>的问题。

🔎

延伸解读

Promise 自动展平带来的类型陷阱

JavaScript 的 Promise 会将 Promise<Promise<T>> 自动展平为 Promise<T>,这违反了单子定律。在 Channel<T> 的 pop() 方法中,如果 T 包含 Promise<undefined>,返回类型 Promise<T | undefined> 会变成 Promise<undefined>,导致无法区分正常值和空值。这种问题在 TypeScript 中不会报错,开发者容易忽略。

Maybe 包装如何保留类型结构

通过将 pop() 的返回类型改为 Promise<Maybe<T>>,其中 Maybe<T> = Just<T> | undefined,Just<T> = { item: T },可以避免 Promise 的自动展平。因为 Promise 不会立即嵌套在 Maybe 内部,结构得以保留。这样 Channel<undefined> 和 Channel<Promise<undefined>> 都能正常工作,无需额外限制 T 的类型。

对开发者的实际影响

使用 Channel<T> 时,若 T 可能包含 undefined 或 Promise<undefined>,原有的 pop() 签名会导致类型混淆。TypeScript 不会警告,问题隐蔽。采用 Maybe 包装后,类型系统能正确区分空值和有效值,减少了运行时错误的风险,提升了代码的可靠性。

❓

Q&A

为什么说 JavaScript 的 Promise 不是单子?

因为 JavaScript 的 Promise 会将 Promise<Promise<T>> 自动展平为 Promise<T>,例如 await Promise.resolve(Promise.resolve(123)) 的结果是 123,而不是嵌套的 Promise。

在 Channel<T> 中使用 Promise<T | undefined> 作为 pop() 的返回类型有什么隐患?

如果 T 包含 undefined,则无法区分 pop() 返回的是 T 还是 undefined;更隐蔽的是,如果 T 包含 Promise<undefined>,由于 Promise 的展平特性,Promise<Promise<undefined> | undefined> 会变成 Promise<undefined>,导致类型信息丢失。

为什么 TypeScript 不会警告 Promise<Promise<undefined>> 被展平的问题?

因为 TypeScript 的类型系统没有对 Promise 的自动展平行为进行特殊检查,它不会警告这种类型混淆,开发者可能难以察觉。

如何解决 Channel<T> 中 pop() 返回类型因 Promise 展平而丢失结构的问题?

将 pop() 的签名改为 Promise<Maybe<T>>,其中 Maybe<T> = Just<T> | undefined,Just<T> = { item: T }。这样即使 T 是 Promise<undefined>,返回类型 Promise<{ item: Promise<undefined> } | undefined> 也不会被展平,保留了结构。

Maybe<T> 类型是如何定义的?

Maybe<T> 定义为 Just<T> | undefined,其中 Just<T> 是一个包含 item: T 属性的对象类型。

使用 Maybe<T> 后,Channel<undefined> 和 Channel<Promise<undefined>> 能正常工作吗?

是的,使用 Maybe<T> 后,Channel<undefined> 和 Channel<Promise<undefined>> 都能按预期工作,不再需要限制 T 不能包含 undefined 或 Promise<undefined>。

🏷️

标签

➡️

继续阅读