typescripttype-parameter

Why does typescript allow this kind of circular reference when using generic types?


Why does typescript not complain about the definition of the Document interface below. It seems like a circular reference. How can a type parameter of "Document" be supplied while writing the definition of the Document interface? And how will I ever be able to create a Document object (seems like an infinite circular reference)

 interface ValueObject<T> {
  value: T;
  validate(): Boolean
}

interface Document extends ValueObject<Document> {
    value: Document
}

Solution

  • Finding a implementation that compiles is trivial (I renamed Document to FooDocument to avoid the browser-side Document-type)

     interface ValueObject<T> {
      value: T;
      validate(): Boolean
    }
    
    interface FooDocument extends ValueObject<FooDocument> {
        value: FooDocument
    }
    
    class Foo implements FooDocument {
      value: FooDocument;
    
      constructor() {
        this.value = new Foo();
      }
    
      public validate() {
        return true;
      }
    }
    

    Now, this is obviously going to end in an endless loop of new Foos being created. However, consider this slightly altered example:

    
    class Foo implements FooDocument {
      value: FooDocument;
    
      constructor(x?: FooDocument) {
        if (x == undefined) {
          this.value = new Foo(this);
          console.log(1);
        } else {
          this.value = x;
          console.log(2);
        }
      }
    
      public validate() {
        return true;
      }
    }
    
    new Foo();
    

    Now the Foo constructor is called without a parameter, which then calls the constructor with a parameter, such that in the end the constructor is called twice. This program therefore does not only fulfill your type definition, but also terminates without an endless loop.

    And if you slightly modify this, then you can see that this is actually very similar to a Linked List:

     interface CoolList<T, S> {
      next: T | undefined;
      value: S;
    }
    
    interface FooDocument extends CoolList<FooDocument, number> {
        next: FooDocument | undefined;
        value: number;
    }
    
    class Foo implements FooDocument {
      value: number;
      next: FooDocument | undefined;
    
      constructor(val: number, next?: FooDocument) {
        this.value = val;
        this.next = next;
      }
    
      
    }
    
    const last = new Foo(3);
    const second = new Foo(2, last);
    const first = new Foo(1, second);
    
    console.log(first.next?.next?.value); // 3
    

    These are generally called recursive data types. Another example of this are ring buffers/circular lists (where the last next would point to the first element, rather than undefined), or trees, where you would have multiple next properties. It thus makes perfect sense, that Typescript allows this.