【问题标题】:Implementing Maybe/Option type in TypeScript using class inheritance使用类继承在 TypeScript 中实现 Maybe/Option 类型
【发布时间】:2022-01-05 16:42:14
【问题描述】:

我见过的大多数 Maybe/Option 类型的 Typescript 实现要么使用带有标签的接口来区分 Some/None 变体。我见过一些没有并且实际上有 Some 和 None 的单独类,它们都实现了一个通用的 Option 接口。作为一个实验,我想尝试一个使用继承的实现。所以这是我想出的简化版本:

abstract class Maybe<T> {
    constructor(private value: T) {}

    isSome(): this is Some<T> {
        return this instanceof Some;
    }
  
    isNone(): this is None {
        return !this.isSome();
    }

    and<U>(other: Maybe<U>): Maybe<U> {
        return this.isNone() ? new None() : other;
    }

    unwrap(): T {
        if (this.isNone()) throw new Error('called unwrap() on None');
        return this.value;
    }

    unwrapOr(defaultValue: T): T {
        return this.isNone() ? defaultValue : this.value;
    }
}

class Some<T> extends Maybe<T> {
    constructor(value: T) { super(value); }
}

class None extends Maybe<any> {
    constructor() { super(null); }
}

但是我在None 类上将类型传递给extends Maybe&lt;...&gt; 时遇到了一些问题。我曾尝试使用nevernullany,它们都会导致(不同的)编译器错误,迫使我在Maybe's 方法中进行一些奇怪的转换。

你可以找到上面代码here的游乐场。

这是我的想法。 Maybe 上的泛型 Type T 表示包含值的类型,可以是任何类型。但是当MaybeNone 时,它确实应该是never,因为从概念上讲,它没有包含值。与此保持一致,我的第一个想法是让None 扩展Maybe&lt;never&gt;,但随后对super(null) 的调用会触发编译器错误,因为null 不能分配给类型never

我想出的最有用的版本是 None 扩展 Maybe&lt;any&gt; 的版本,但这需要在 Maybes 方法中进行一堆 (&lt;Some&lt;T&gt;this).value 的转换(上面的代码中没有显示),我不喜欢那样。理想情况下,我想找到一个不需要使用any 并在各处进行强制转换的实现。任何有助于实现这一目标的帮助将不胜感激。

【问题讨论】:

  • Ehh,确实没有可以分配给never 的值,因此您可以编写super(null!) 并在那里进行一种类型的断言并继续前进。或者你可以重构,这样基类就不会假装做它不能做的事情(比如保持never值),比如this。不确定您想在此处做什么或将其视为答案。
  • @jcalz 感谢您的快速反馈。我从未想过在super(null) 调用中直接进行! 类型断言。但实际上,像你建议的那样重构类可能是要走的路。所以如果你想写一个答案,我会去的。

标签: typescript class inheritance optional maybe


【解决方案1】:

让代码正常工作所需的最小更改可能是这样的:

class None extends Maybe<never> {
    constructor() { super(null!); }
}

也就是说,我们说NoneMaybe&lt;never&gt;,其中the never type 是没有值的bottom type。从类型系统的角度来看,这是合理的:“正确”类型类似于 forall T. Maybe&lt;T&gt;,但由于 TypeScript 没有像 microsoft/TypeScript#17574 这样的“通用值”,因此无法直接表达这种类型。因为看起来Maybe&lt;T&gt; 应该是T 中的covariant,那么你可以将forall T. Maybe&lt;T&gt; 改写为Maybe&lt;forall T. T&gt;,这相当于Maybe&lt;never&gt;,因为TypeScript 中所有类型的交集是never

但是为了构造你的代码Maybe&lt;never&gt;,你需要提供一个never类型的值给超级构造器。这当然不可能发生。您传递的是null,它不是never。我们可以使用non-null assertion operator (postfix !)null 向下转换为nevernull! 的类型是NonNullable&lt;null&gt;,即never)。从技术上讲,这是一个谎言,但它是一个相当无害的谎言,尤其是如果您以后从不尝试观察该值。


如果你想在任何地方都输入安全,那么我认为你需要重构你的基类,这样它就不会假装做它不能做的事情。 Maybe&lt;T&gt; 可能没有 value,因此基类中可能不需要 value。可能是optional:

abstract class Maybe<T> {
    constructor(public value?: T) { } // <-- optional

    unwrap(): T {
        if (this.isSome()) return this.value; // reverse check
        throw new Error('called unwrap() on None');
    }

    unwrapOr(defaultValue: T): T {
        return this.isSome() ? this.value : defaultValue; // reverse check
    }
}

class Some<T> extends Maybe<T> {
    declare value: T // <-- narrow from optional to required
    constructor(value: T) { super(value); }
}

class None extends Maybe<never> {
    constructor() { super(); } // <-- don't need an argument now
}

然后你可以 declare the property in the subclass 被要求(我必须使它成为 public 因为 private 不让子类查看事物,甚至 protected 与你拥有的那些类型保护函数变得不确定) .请注意,我将您的检查从 isNone() 切换到 isSome()... Maybe&lt;T&gt; 类不知道是 Some&lt;T&gt; | Noneunion,因此您不能使用 isNone() 的错误结果得出this.value 存在的结论。相反,您应该使用isSome() 的真实结果。


最后,您可以将所有Some/None 特定功能从Maybe 中移出,然后不再担心试图强制基类表现得像两个子类一样。基于继承的多态性倾向于使用子类来覆盖方法,而不是使用检查instanceof 的超类方法。这类似于让Maybe 只是一个interface,除了不需要检查当前类的任何真正的多态方法:

abstract class Maybe<T> {
    abstract isSome(): this is Some<T>;
    abstract isNone(): this is None;
    abstract and<U>(other: Maybe<U>): Maybe<U>;
    abstract unwrapOr(defaultValue: T): T;
}

class Some<T> extends Maybe<T> {
    private value: T
    constructor(value: T) { super(); this.value = value; }
    isSome(): this is Some<T> { return true; }
    isNone(): this is None { return false; }
    and<U>(other: Maybe<U>) {
        return other;
    }
    unwrap() { return this.value }
    unwrapOr(defaultValue: T) { return this.value }
}

class None extends Maybe<never> {
    isSome<T>(): this is Some<T> { return false }
    isNone(): this is None { return true }
    and<U>(other: Maybe<U>) {
        return this;
    }
    unwrapOr<T>(defaultValue: T) { return defaultValue }
}

这里,Maybe 中除了抽象方法之外什么都没有。如果你能想出一些不需要检查this.isSome()this.isNone() 的东西,那么它可以放在基类中。这里唯一值得注意的是Maybe&lt;never&gt; 的一些子类化涉及将非泛型方法(isSomeunwrapOr)转换为泛型方法。


Playground link to code

【讨论】:

  • 好东西。感谢您的详细回答。我在使用这些选项时遇到的一个(小)“问题”是编译器无法推断可以返回 Some&lt;T&gt; 和 None 作为 Maybe&lt;T&gt; 的函数的类型。相反,它将返回类型推断为Some&lt;T&gt; | None。这搞砸了进一步的方法链接。我想知道是否有办法让编译器将该联合识别为等同于Maybe&lt;T&gt;。我在你的playground 中添加了一个andThen 方法来说明我的意思。
  • 显然,一个简单的解决方案是从该函数显式返回Maybe&lt;T&gt;。但我很好奇是否有办法让编译器不需要它。
  • 只考虑类型,重构版本,抽象类基本上是一个接口,看起来是“最干净的”。但是,您不妨使用一个界面。另一种方法是让抽象类实现所有这些方法的None 变体(因为它不需要访问value),然后让Some 覆盖它们。 None 类将为空并且仅扩展 Maybe。你会节省一点代码。不过不确定我喜欢它。
  • 首先,Maybe&lt;T&gt;Some&lt;T&gt; | None 更宽,因为Maybe&lt;T&gt; 的其他子类型可能既不是Some&lt;T&gt; 也不是None(抽象超类不仅仅是“已知”子类;这就是为什么我提到检查this.isNone() === false 并不意味着this.isSome() === true。可能存在符合Maybe&lt;T&gt; 的其他类型,但两者都不是。如果你关心Maybe&lt;T&gt;,你应该正如您所提到的,通过注释手动扩展您的类型。
猜你喜欢
  • 1970-01-01
  • 2020-01-05
  • 2012-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多