Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reference type of self in Typescript interface (for a IClonable interface)

Tags:

typescript

I need an IClonable interface that defines a clone() member, that returns an instance of the class that implemented it.

If possible, how can I indicate that the return type of clone() will be the same as the class it is called on?

interface IClonable {
    clone(): ???
}

I know I can do this with generics like below, but that seems overly verbose

interface IClonable<T> {
    clone(): T
}
like image 206
altschuler Avatar asked Apr 20 '16 12:04

altschuler


People also ask

Can an interface reference itself TypeScript?

When writing out a type or interface in TypeScript for something that will be fed into a recursive function you could cop out and use any or you could properly define the structure. Fortunately, both type and interface allow you to be self referential in terms of defining properties.

Can an interface extend a type TypeScript?

TypeScript allows an interface to extend a class. In this case, the interface inherits the properties and methods of the class. Also, the interface can inherit the private and protected members of the class, not just the public members.

Is type of interface TypeScript?

TypeScript Interface TypeTypeScript allows you to specifically type an object using an interface that can be reused by multiple objects. To create an interface, use the interface keyword followed by the interface name and the typed object.

Can interface extend two interfaces TypeScript?

You can extend from as many interfaces as necessary by separating the interfaces with a comma. You are not required to add any new members to the final interface and can use the extends keyword to simply combine interfaces.


2 Answers

Update Oct 11th, 2022

The previous answer had a flaw that only shows up when the inheritance chain grows beyond our simple example. Consider this example:

class B extends A {
  foo() {
    console.log("Foobar");
  }
}

const b = new B(3).clone() // type: B ???
b.foo(); // RuntimeError: b.foo is not a function 

Playground link

There is a problem here that won't be caught at compile time, and only during runtime will we see it. Therefore, I propose yet another alternative:

Solution

interface ICloneable {
    clone(): ThisType<this>;
}

class A implements ICloneable {
  constructor(readonly a: number){}

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

Usage:

const a = new A(42).clone(); // type: A

class B extends A {
  foo() {
    console.log("Foobar");
  }
}

const b = new B(3).clone() // type: A

// @ts-expect-error
b.foo();

By using a contextualized this type, the return type for clone is bound to the context within which it was declared, and not the context within which it was called.

Now if we remove the // @ts-expect-error comment, typescript will catch this error at runtime, because the clone method we have in B does not return type B.


Previous answer:

Use (polymorphic) this as the return type:

interface ICloneable {
    clone(): this;
}

In classes, a special type called this refers dynamically to the type of the current class.

Usage:

class A implements ICloneable {
  constructor(readonly a: number){}

  clone() {
    return new A(this.a) as this;
  }
}
like image 180
smac89 Avatar answered Oct 23 '22 23:10

smac89


You are right, using generics is the way to do it, even if it's verbose..

You can also:

interface IClonable {
    clone(): any;
}

or

interface IClonable {
    clone(): any;
    clone<T>(): T;
}

or

interface IClonable {
    clone(): IClonable;
}

But using generics is probably the best way to go.


Edit

@Silvermind comments below made me check the suggested code of clone<T>(): T and I was wrong as it does not compile.
First Here's a code to show what I meant:

class Point implements IClonable {
    private x: number;
    private y: number;
    
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    
    public clone<T>(): T {
        return <T> new Point(this.x, this.y);
    }
}

var p1 = new Point(1, 2);
var p3 = p1.clone<Point>();

Basically to perform the cast in the clone method instead of casting the returned value.
But <T> new Point(this.x, this.y) produces:

Neither type 'Point` nor type 'T' is assignable to each other

like image 4
Nitzan Tomer Avatar answered Oct 23 '22 23:10

Nitzan Tomer