microsoft / microsoft/TypeScript

Brittle circularity inherited from factory-produced class

Open
#38,476 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Needs Investigation
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

TypeScript Version: 3.8.3

Search Terms: extend, from, circular, constructor, class, factory

The following works like a charm. I'm able to define a circular relationship between RecordShape and ArrayShape. This is wonderful!

class ArrayShape {
  constructor(public of: any) { }
}

namespace ArrayShape {
  export function of(of: any) {
    return class extends ArrayShape {
      constructor() {
        super(of);
      }
    }
  }
}

class RecordShape {
  constructor(public of: object) { }
}

namespace RecordShape {
  export function of(of: any) {
    return class extends RecordShape {
      constructor() {
        super(of);
      }
    }
  }
}

class CycleNodeA extends RecordShape.of(() => CycleNodeB) {}
class CycleNodeB extends ArrayShape.of(() => CycleNodeA) {}

However, this starts to break down as I make it more generic. In the example below, I begin to narrow down the type passed into the ArrayShape and RecordShape constructors. The result is––once again––circularity errors :/ here is the playground.

function TypeOnly<T>() {
  return (undefined as any) as T;
}

// the base from which `ArrayShape` and `RecordShape` extend
abstract class Shape<N extends string, T> {
  abstract readonly name: N;
  abstract readonly type: T;
}

// extracts the `T` from a given shape
type DecodeShape<S extends Shape<string, any>> =
  S extends Shape<string, infer T>
    ? T
    : never;

// for referencing shapes which aren't yet defined
type PointerShape = (() => Shape<string, any>);

// utils for treating shape and pointer shape types as if they are the same
type ShapeLike = Shape<string, any> | PointerShape;
type NormalizeShape<S extends ShapeLike> =
  S extends PointerShape
    ? S extends (() => infer SS) ? SS : never
    : S;
type DecodeShapeLike<S extends ShapeLike> = DecodeShape<NormalizeShape<S>>;

class ArrayShape<T extends ShapeLike> extends Shape<"array", DecodeShapeLike<T>[]> {
  readonly name = "array";
  readonly type = TypeOnly<DecodeShapeLike<T>[]>();

  constructor(public elementShape: T) {
    super()
  }
}

namespace ArrayShape {
  export function of<T extends ShapeLike>(elementShape: T) {
    return class extends ArrayShape<T> {
      constructor() {
        super(elementShape);
      }
    }
  }
}

class RecordShape<T extends Record<string, ShapeLike>> extends Shape<"record", {
  [K in keyof T]: DecodeShapeLike<T[K]>;
}> {
  readonly name = "record";
  readonly type = TypeOnly<{
    [K in keyof T]: DecodeShapeLike<T[K]>;
  }>();

  constructor(public fieldShapes: T) {
    super();
  }
}

namespace RecordShape {
  export function of<T extends Record<string, ShapeLike>>(fieldShapes: T) {
    return class extends RecordShape<T> {
      constructor() {
        super(fieldShapes);
      }
    }
  }
}

const CycleNodeA = RecordShape.of({
  b: () => new CycleNodeB()
}) {}
class CycleNodeB extends ArrayShape.of(() => new CycleNodeA()) {}

If the circularity error turns out to be the intended behavior, apologies––I can post on StackOverflow instead. Otherwise, any help would be greatly appreciated! Thank you!

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce both linked TypeScript Playground examples with TypeScript 3.8.3, comparing the working circular example with the generic version that reports circularity errors. Start by investigating how the type checker handles recursive types through factory-produced classes and conditional or mapped types. Done means determining whether the diagnostic is intended and, if not, covering the reported behavior with a regression test and correcting it.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.