0

I have a set of classes which can be "naively" represented as a diamond:

  OneBase
 /      \
OneDer   TwoBase
 \      /
  TwoDer

[NB -- due to @mitch comment have changed names.]

In fact, One and Two are "base" classes, and there are several different classes besides OneDer and TwoDer that derive from them using the same pattern. Lets say that (OneBase,TwoBase) are in module base, and (OneDer, TwoDer) are in module derived.

Also OneBases contain a structure Part which contains a TwoBase instance; in OneDers the equivalent structure should contain a TwoDer instance.

I converted TwoBase into a mixin, and the following code does compile:

Module base:

export type Constructor<T> = new(...args: any[]) => T

export class One {
  p: Part
  constructor (readonly a: number) {}
}

export class TwoPre extends One {
  constructor (readonly a: number, readonly c: M[]) {
    super(a)
  }
}
export function TwoMix<
    T extends Constructor<TwoPre>> (Base: T): Constructor<TwoPre> {
  class Two extends Base {

  }
  return Two
}
export const Two = TwoMix(TwoPre)
export interface Part {
  u: typeof Two
}

Module derived:

import { Part, One, TwoMix } from './base'

export class OneDer extends One {
  p: Part
}

export class TwoDerPre extends OneDer {
  constructor (readonly a: number, readonly c: M[]) {
    super(a)
  }
}

export const TwoDer = TwoMix(TwoDerPre)

Note that TwoPre and TwoDerPreare necessary because the "naive" Two has a different constructor signature than One and mixin functions can't define a constructor. This is a bit of a pain, as it adds unnecessarily to the prototype chain -- so workarounds appreciated. Other than this, the mixin does enforce the method resolution order I would like.

The real problem comes when I try to enforce the difference of PartDer from Part; this version of module derived doesn't compile:

import { Part, One, TwoMix } from './base'

export class OneDer extends One {
  p: PartDer
}

export class TwoDerPre extends OneDer {
  constructor (readonly a: number, readonly c: M[]) {
    super(a)
  }
}

export const TwoDer = TwoMix(TwoDerPre)
export interface PartDer extends Part {
  u: typeof TwoDer
}

I get the errors: src/derived.ts(13,14): error TS7022: 'TwoDer' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. src/derived.ts(15,3): error TS2502: 'u' is referenced directly or indirectly in its own type annotation.

I'm wondering why the compiler complains about this cycle when it doesn't complain about it in base, and what I can do about it?

NOTE This is the shortest way I could think of to express the problem. Of course, TwoDer is really not just the instantiation of the mixin, so the code at the end of derived looks more along the lines of:

export class TwoDer extends TwoMix(TwoDerPre) {

}
export interface PartDer extends Part {
  u: TwoDer
}

With this version I get slightly different errors: src/derived.ts(17,6): error TS2304: Cannot find name 'TwoDer'. src/derived.ts(17,6): error TS4033: Property 'u' of exported interface has or is using private name 'TwoDer'. If I convert PartDer to use typeof TwoDer I get the original errors:

export interface PartDer extends Part {
  u: typeof TwoDer
}
shaunc
  • 5,317
  • 4
  • 43
  • 58
  • The single-letter class names make it pretty much unreadable. I'm guessing the long story short is you're trying to implement multiple inheritance. This answer here looks fairly popular: https://stackoverflow.com/questions/26948400/typescript-how-to-extend-two-classes – Mitch Nov 18 '17 at 05:29
  • Sorry about the names -- the real names wouldn't make sense so I just used their first letters M and G, with "B" for base and "D" for derived. I did see that answer a while ago and am using mixins -- so this isn't a "generic" problem, but "why does the mixin I wrote not work?" – shaunc Nov 18 '17 at 05:40
  • Ok ... changed the names... – shaunc Nov 18 '17 at 06:35

1 Answers1

1

I can't quite fathom what you are trying to do, but TypeScript loses a bit of type information here:

export const TwoDer = TwoMix(TwoDerPre)

And this is causing a knock on effect. You can solve this specific issue by adding some type information:

export const TwoDer: Constructor<TwoPre> = TwoMix(TwoDerPre)

This clears the problem with 'u' is referenced directly or indirectly in its own type annotation. confusion. I have no idea if it makes everything work for you.

Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Brilliant -- thanks! Modelling this pattern has been a challenge because typescript mixin support is still very fragile. For instance, can have abstract classes and can't export mixins with private variables. But there seem to be tracking issues in typescript github for those things. I couldn't think what I was running afoul of in this one. Perhaps some more documentation about why this extra type annotaton is necessary would be useful, as it would seem to me to be inferable. – shaunc Nov 18 '17 at 16:28
  • Mixin support is better now in 2019, but still fragile. Another thing you can do in a mixin (f.e. your `TwoMix`) is `return Two as typeof Two & T`, and don't annotate the return type so that it infers the return value from whatever you pass into it. – trusktr Jun 30 '19 at 20:41