gpt4 book ai didi

javascript - 使用 Typescript 在 Opaque 上区分联合

转载 作者:行者123 更新时间:2023-12-01 15:17:40 25 4
gpt4 key购买 nike

Typescript 没有内置的 Opaque类型 like Flow does .所以我做了自己的定制Opaque类型:

type Opaque<Type, Token = unknown> = Type & {readonly __TYPE__: Token}

它可以完成工作,但与此同时,我正在失去 Discriminating Union 的能力.我举个例子:

我有一系列动物和以下不透明:

animals constant

enter image description here

到目前为止,一切都很好。正确的?嗯……不完全是。我正在使用 assertNever这有助于我将值断言为 never (在区分工会时有用):
export function assertNever(value: never): never {
throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
}

但是因为 __TYPE__属性(property),我不能真正“区分”:

enter image description here

我做了 a simple demo on codesandbox说明问题,或者您可以在此处查看完整源代码:
export function assertNever(value: never): never {
throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
}

type Opaque<Type, Token = unknown> = Type & { readonly __TYPE__: Token };

const animals = ["Dog" as const, "Cat" as const];

type Animal = Opaque<typeof animals[0], "Animal">;


function makeSound(animal: Animal) {
switch (animal) {
case "Dog":
return "Haw!";
case "Cat":
return "Meow";
default:
assertNever(animal);
// ^^^ discriminated unions won't work.
}
}

makeSound("Dog" as Animal);

非常感谢您的帮助或建议。

最佳答案

我还没有弄清楚为什么你的Opaque<T, U>使用 if 时,类型无法通过控制流分析正确缩小范围/elseswitch/case陈述。当您传入 T 的原始数据类型时在 Opaque<T, U> ,如 Animal"Cat" | "Dog" ,您会得到所谓的“品牌原语”,如 in this FAQ entry about making nominal types 所述.似乎正在发生的事情是当你有一个品牌原语 val并对另一个原始值 somePrimitive 使用常规类型保护检查,如 if (val === somePrimitive) { /*true*/ } else { /*false*/ }switch (val) { case somePrimitive: /* true */ break; /*false*/ },检查的“真实”部分一切正常:val 的类型缩小到 Extract< typeof val, typeof somePrimitive> .所以在你的情况下,if (animal === "Dog") { /*true*/ } else { /*false*/ }缩小到 Opaque<"Dog", "Animal">在真正的分支。

不好的是检查的“错误”部分发生了什么。如果 val不等于 somePrimitive ,那么我们应该能够将其缩小到 Exclude< typeof val, typeof somePrimitive> .也就是说,当 animal不等于 "Dog" ,编译器应该缩小 animalOpaque<"Cat", "Animal"> .但这并没有发生。

有时在这样的检查中,不缩小错误分支是正确的。例如,当您的类型不是单例并且可以具有多个该类型的有效值时。如果我有 function f(x: string | number) { if (x === "Dog") { /*true*/ } else { /*false* } } , 缩小 x 是有意义的至string (甚至 "Dog" )在真正的分支中,但你不想缩小 xnumber在假分支中。当编译器不确切知道发生了什么时,最安全的做法是缩小真分支​​而不是缩小假分支。

但我没想到编译器会在品牌原语的情况下采用这条路线。 animal 的类型不可能成为 Opaque<"Dog", "Animal">一旦你有animal !== "Dog" .所以我倾向于提交一个关于这个问题的 GitHub 问题,看看他们怎么说;感觉要么是错误,要么至少是设计限制。我有点惊讶我之前没有看到这个问题,而且我找不到已经提交的直接相关问题。那好吧。

那么,有哪些可能的解决方法?一种是制作user-defined type guard function .用户定义的类型保护通常由编译器处理,即使是 false结果意味着参数的缩小。这并不总是可取的(请参阅 microsoft/#15048 以获取允许此类类型保护函数更具可配置性以便 false 不会缩小返回值的建议),但这正是您想要的。它可以这样实现:

function is<T extends string>(x: any, v: T): x is T {
return x === v;
}
function makeSound(animal: Animal) {
if (is(animal, "Dog")) {
return "Haw!";
} else if (is(animal, "Cat")) {
return "Meow"!
} else {
assertNever(animal); // no error now
}
}

这行得通。当然,正如您所提到的,它需要重构您的所有 switch/ case函数调用语句 if/ else陈述,所以它可能太痛苦了。

理想情况下,TypeScript 将支持更正式的不透明/标称类型,例如 unique type brand proposal in microsoft/TypeScript#33038 .但现在我能想到的最简单的解决方法是让您保留 switch语句是使用 string enum .

通常我根本不推荐使用枚举,因为它们有奇怪的警告并且不符合当前的 design methodology of TypeScript (枚举是纯 JavaScript 中不存在的运行时功能,与非目标 #6 相冲突)......但至少在用作判别式时它们的行为符合预期:
enum Animal {
DOG = "Dog",
CAT = "Cat"
}

function makeSound(animal: Animal) {
switch (animal) {
case Animal.DOG:
return "Woof!"; // English-speaking dog
case Animal.CAT:
return "Meow!";
default:
assertNever(animal); // no error
}
}

在这里你的 Opaque , animals , 和 Animal被单个 Animal 替换枚举。请注意,在 makeSound我们必须针对 Animal.DOG 进行测试和 Animal.CAT而不是反对 "Dog""Cat" .否则编译器仍然不会进行错误案例缩小。幸运的是,检查枚举值确实有效。

所以,这些是我的想法。希望他们能帮助你继续。祝你好运!

Playground link to code

关于javascript - 使用 Typescript 在 Opaque 上区分联合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61297080/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com