4

I am having an issue using recursive generics.

I created an interface using generics. The idea is a branch can have many child branches which are also generics. This is my code ( I cut it down for posting)

import Contact from 'Business/Models/Contact';
import Office from 'Business/Models/Office';

export interface BranchInterface<T> {
    children: Array<BranchInterface<T>>;
    record: T;
}

export class Branch<T> implements BranchInterface<T> {

    public children: Array<BranchInterface<T>> = [];

    constructor(public record: T) {}
}


const newOffice = new Office();

const rootBranch = new Branch<Office>(newOffice);

rootBranch.children = Array<Branch<Contact>>(); << is the issue

The issue is that when I construct an Office branch, the children array is being constructed as Office type.

This means when I try to assign an array of Contact branches as the children of the Office branch typescript throws the following error;

Type 'Branch<Contact>[]' is not assignable to type 'Branch<Office>[]'...

The reason for doing this is because the type of branches are unknown and I don't really want to use any either if I can avoid it.

So, How would I resolve this?

3
  • If your Array is of generic type T then you've already decided the children are the same type as the branch. Use a different generic type identifier for your Array, and create a parent type for all your branch types. Commented Aug 29, 2018 at 14:32
  • "the type of branches are unknown" If they are unknown then why are you trying to give them a type other than Branch<unknown> or Branch<any>? Commented Aug 29, 2018 at 16:02
  • Sorry, by unknown I mean they can be of any class definition, but I don't want to set it to any because I need to maintain the type. if the set is constructed as X then you can't change to Y. However here I don't want the children to construct as the same type as the parent. If that makes sense? Commented Aug 29, 2018 at 16:04

2 Answers 2

5

So, if you really want to strongly type Branch, you need to give it a type corresponding to all the nested levels of children. That would look like a list or tuple of types. Since TypeScript 3.0 introduced tuple types in rest/spread expressions, you can kind of express this, but I don't know if it's worth it to you.

First, let's define the type functions Head and Tail which split a tuple type into its first element and a tuple of the rest of the elements:

type HeadTail<L extends any[]> = 
  ((...args: L) => void) extends ((x: infer H, ...args: infer T) => void) ? [H,T] : never
type Head<L extends any[]> = HeadTail<L>[0];
// e.g., Head<[string, number, boolean]> is string
type Tail<L extends any[]> = HeadTail<L>[1]; 
// e.g., Tail<[string, number, boolean]> is [number, boolean]

Now we can define BranchInterface or Branch to take a tuple of types like this:

export interface BranchInterface<T extends any[]> {
  children: Array<BranchInterface<Tail<T>>>
  record: Head<T>;
}

export class Branch<T extends any[]> {
  public children: Array<BranchInterface<Tail<T>>> = [];
  constructor(public record: Head<T>) { }
}

Assuming you know that you want the top level to be an Office and the next level down to be a Contact, then you can define your list of types as [Office, Contact] and see if it works:

const rootBranch = new Branch<[Office, Contact]>(newOffice);
const anOffice = rootBranch.record; // Office
const aContact = rootBranch.children[0].record; // Contact

Of course if you traverse past that, you find out what Head<[]> is (that implementation gives {}, I guess):

const whoKnows = rootBranch.children[0].children[0].record; // {}

If you want to make the layers below Contact be something like never instead (because you will never traverse down that far), you can use a rest tuple like this:

const rootBranch = new Branch<[Office, Contact, ...never[]]>(newOffice);
const anOffice = rootBranch.record; // Office
const aContact = rootBranch.children[0].record; // Contact
const aNever = rootBranch.children[0].children[0].record; // never
const anotherNever = rootBranch.children[0].children[0].children[0].record; // never

Note that this requires you to explicitly specify the type parameter T when constructing a Branch, since the compiler cannot infer the type from the argument:

const oops = new Branch(newOffice); 
oops.record; // any, not Office

Well, it works. Up to you if you want to go that way. Hope that helps; good luck!

Sign up to request clarification or add additional context in comments.

Comments

0

It seems like you may be using Generics where simple inheritance would do. If all you need is for an office or a contact branch to have a list of branch children, then the following code works using only a generic Array.

export class Branch {
  children: Array<Branch>;
  constructor(){
    this.children = new Array<Branch>();
  }
}

class Office extends Branch {}
class Contact extends Branch {}

const newOffice = new Office();
newOffice.children.push(new Contact());

However if you are intent on using generics, you need 2 generic types in your Branch class to decouple the record and children types, like such:

export class Branch<T, U> {

  public children: U[];

  constructor(public record: T) {
    this.children = [];
  }
}

class Office {};
class Contact {};

const newOffice = new Office();

const rootBranch = new Branch<Office, Contact>(newOffice);

rootBranch.children = new Array<Contact>();

4 Comments

That would circle back to the same issue. rootBranch.children = new Array<Contact>(); fails because now children has to be <Office, Contact> and can't be just <Contact> or even <Contact, any>
Actually, your solution works if I change it to <T, U = {}>
With the above, rootBranch.children = new Array<Contact>(); also needs to be rootBranch.children = new Array<Contact, any | a type >();
@MokkyMiah I tested my code and it works as is. However the children are of type Contact, not Branch. If the children have to be of type Branch then they must also specify the type of their own grandchildren. This is by design. If the children do NOT have children of their own, then they cannot be of type Branch.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.