Consider a typescript interface that describes the constraint that an object has a .clone()
method that returns an object of the same type (or potentially an object assignable to its own type). An example of what I'm imagining:
// 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])];
While this works, the interface fails to capture our understanding of how a Cloneable should behave: it admits a class Faker
whose .clone()
method returns a cloneable object of some other class that is unassignable to type Faker
. To solve that problem we can parameterize the Cloneable interface by type:
// 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;
}
and then to constrain a type parameter to cloneable we can specify T extends Cloneable<T>
:
// Interface that requires a cloneable
interface RequiresCloneable<T extends Cloneable<T>> {
//...
}
All of the three parameterized interfaces admit class A
. However we lose the ability to specify that a particular value should be Cloneable without knowing its implementation type:
const clonableA: Cloneable<A> = new A([1]);
const clonableOfUnknownType: Cloneable<unknown>; // admits Fakers with approach 1 and 2, doesn't compile with approach 3
I'm trying to wrap my head around how the type system works and what is possible and impossible under the type system. My questions are:
- Is there any way to make a non-generic Cloneable interface or type that captures the idea that the object's clone should be the same type as itself? If not, what properties of the typescript type system makes this so?
- Out of the three generic
Cloneable<T>
interfaces I described above, which one is the best? Best potentially meaning clearest, simplest, most concise, most idiomatic, or strictest. It seems that the third is the only interface that doesn't admit a faker. Is there any better way to do what they are doing? - Is there a name for the pattern
interface S<T extends S<T>>
in typescript, or more generally in any type system?