【问题标题】:Creating a generic "Cloneable" interface in typescript在打字稿中创建一个通用的“可克隆”接口
【发布时间】:2021-03-06 04:42:55
【问题描述】:

考虑一个 typescript 接口,它描述了一个对象有一个 .clone() 方法的约束,该方法返回一个相同类型的对象(或者可能是一个可分配给它自己类型的对象)。我想象的一个例子:

// Naive definition
interface Cloneable {
  clone: () => Clonable;
}

class A implements Cloneable {
  a: number[];
  constructor(a: number[]) {
    this.a = a;
  }
  clone(): A {
    return new A(this.a.slice()); // returns a deep copy of itself
  }
}

interface RequiresCloneable<T extends Cloneable> {
  //...
}

const arrayOfCloneables: Clonable[] = [new A([1,2,3])];

虽然这可行,但该接口未能捕捉到我们对 Cloneable 应如何表现的理解:它承认一个类 Faker,其 .clone() 方法返回某个其他类的可克隆对象,该对象无法分配给类型 Faker。为了解决这个问题,我们可以按类型参数化 Cloneable 接口:

// Approach 1
interface Cloneable<T> {
  clone: () => T;
}
// Approach 2
interface Cloneable<T> {
  clone: () => Cloneable<T>;
}
// Approach 3. This pattern looks quite strange but this seems to work the best.
interface Cloneable<T extends Cloneable<T>> {
  clone: () => T;
}

然后将类型参数约束为可克隆,我们可以指定T extends Cloneable&lt;T&gt;

  // Interface that requires a cloneable
interface RequiresCloneable<T extends Cloneable<T>> {
  //...
}

所有三个参数化接口都承认类A。但是,我们失去了在不知道其实现类型的情况下指定特定值应该是 Cloneable 的能力:

const clonableA: Cloneable<A> = new A([1]);
const clonableOfUnknownType: Cloneable<unknown>; // admits Fakers with approach 1 and 2, doesn't compile with approach 3

我试图围绕类型系统的工作原理以及在类型系统下什么是可能的和不可能的。我的问题是:

  • 是否有任何方法可以制作非泛型 Cloneable 接口或类型来捕捉对象的克隆应该与自身相同的类型?如果不是,那么 typescript 类型系统的哪些属性会导致这种情况?
  • 在我上面描述的三个通用Cloneable&lt;T&gt; 接口中,哪一个是最好的? Best 可能意味着最清晰、最简单、最简洁、最惯用或最严格。似乎第三个是唯一不承认伪造者的界面。有没有更好的方法来做他们正在做的事情?
  • 在 typescript 中,或更一般地在任何类型系统中是否有模式 interface S&lt;T extends S&lt;T&gt;&gt; 的名称?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    TypeScript 有 polymorphic this types,您可以在其中使用名为 thistype 来指代“与自身相同的类型”。

    interface Cloneable {
        clone(): this;
    }
    

    Cloneable 的子类型的值中,this 的类型将与该子类型相同:

    interface Foo extends Cloneable {
        bar: string;
    }
    declare const foo: Foo;
    const otherFoo = foo.clone();
    otherFoo.bar.toUpperCase();
    

    请注意,当您尝试实际实现 Cloneable 时,编译器会阻止任何试图返回与您已经拥有的相同this 对象不同的东西。在您的class A 中,会发生这种情况:

    clone() { // error! Type 'A' is not assignable to type 'this'.
        return new A(this.a.slice()); // returns a deep copy of itself
    }
    

    这实际上是一个很好的错误,因为您无法选择子类型的底部。有人可以来扩展A(或提供A &amp; SomethingElse 类型的值),this 将随之缩小:

    class B extends A {
        z: string = "oopsie";
    }
    declare const b: B;
    b.clone().z.toUpperCase();
    

    如果您在A 中的clone() 方法没有预见到可能的子类型,例如B,那么它将无法以类型安全的方式工作。无论如何,谈论如何处理这个问题可能是题外话,但如果你并不真正关心可能的子类型,只是希望编译器接受你的实现,你需要一个type assertion,并且在面对子类型:

    clone() {
        return new A(this.a.slice()) as this;
    }
    

    您所说的另一种技术:

    interface Cloneable<T extends Cloneable<T>> {
        clone(): T;
    }
    

    被称为F-Bounded Polymorphism,在这种情况下,“F”只是意味着“功能”。您正在将类型 T 与作为它的函数的另一种类型 F&lt;T&gt; 绑定(在 TS 中,我们称之为 constraining)。它也被称为"recursive bounding"

    请注意,多态 this 是一种隐式的 F 有界多态,其中 this 可以看作是一种“影子”泛型类型参数。与显式类型参数 T 相比,您对 this 的控制更少,这使您可以选择子类型的底部:

    class A implements Cloneable<A> {
    /* ... */
        clone() {
            return new A(this.a.slice());
        }
    }
    
    class B extends A {
        z: string = "oopsie";
    }
    declare const b: B;
    b.clone().z.toUpperCase() // error, no z
    

    这里,b.clone() 产生一个 A 类型的值而不是 B,因为 A implements Clonable&lt;A&gt; 而不是 Clonable&lt;this&gt;

    但是这里的缺点是你需要在任何你想引用Cloneable的地方携带这个“额外的”类型参数:

    const arrayOfCloneables: Cloneable<A>[] = [new A([1, 2, 3])];
    

    Playground link to code

    【讨论】:

    • 我在阅读文档时错过了多态 this!这正是我想要的。您提到在 .clone() 方法中将类型的值转换为 this 会在该类型被扩展时产生问题。似乎要求扩展该类型的子类型必须覆盖 .clone() 方法才能与子类型一起使用,对吗?感谢您的详细回答。
    猜你喜欢
    • 2015-03-24
    • 1970-01-01
    • 1970-01-01
    • 2017-04-15
    • 2021-02-02
    • 2018-02-24
    • 1970-01-01
    • 2017-11-08
    相关资源
    最近更新 更多