shaunc
shaunc

Reputation: 5611

typescript diamond using mixin

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
}

Upvotes: 0

Views: 527

Answers (1)

Fenton
Fenton

Reputation: 250972

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.

Upvotes: 1

Related Questions