'Export a concrete version of a generic TypeScript class

For a module written in TS but consumed in both JS and TS, I have a utility class which (at the point it's defined) is generic with some constraint:

// util/genericthing.ts

interface TOwnerBase {
  someExpectedMethod: { (param: number): void };
}

export class GenericThing<TOwner extends TOwnerBase> {
  owner: TOwner;

  constructor(owner: TOwner) {
    this.owner = owner;
    owner.someExpectedMethod(0);
  }
}

Later, the user-facing owning class is defined with reference to this generic (works around what would otherwise be a circular dependency here):

// main.ts

import { GenericThing } from "./util/genericthing";

export class ThingOwner {
  things: GenericThing<ThingOwner>[];

  someExpectedMethod(param: number) {};
}

export { GenericThing } from "./util/genericthing";

But in expected usage of the module/library, users shouldn't need to care about this generic: TOwner will always be ThingOwner.

This is clunky because users (who would typically just import from main.ts) will need to import and use the pair GenericThing<ThingOwner> if referencing the type. Instead, I'd like them to just reference something like Thing which implicitly resolves the generic.

A type alias like export type Thing = GenericThing<ThingOwner>; doesn't seem to be a good fit, because it can only be used as a type and not a concrete implementation:

// User code
import { Thing, ThingOwner } from "my-cool-module";

// This is fine - because Thing is a type:
let myThing: Thing;

// This is not - because Thing is not a constructor/class:
myThing = new Thing(new ThingOwner());

How could main.ts export a concrete version of Thing that's usable as both a type and an actual class? Or is there some different pattern that should be used here instead?



Solution 1:[1]

Somehow the following works in TypeScript 4.6.3 :

//specificthing.ts
import { GenericThing } from "./genericthing";

export class ThingOwner {
  things!: GenericThing<ThingOwner>[]; 
  someExpectedMethod(param: number) {}; 
}

export { GenericThing } from "./genericthing";

export const Thing = GenericThing;
export type Thing = GenericThing<ThingOwner>;

(Yes, two different exports with the same name Thing: one value and one type).

And it can be used like

import { Thing, ThingOwner } from "./specificthing";

let myThing: Thing = new Thing(new ThingOwner());

Edit: However, Thing will still be detected as having a type parameter, for example a user can see (alias) new Thing<ThingOwner>(owner: ThingOwner): GenericThing<ThingOwner> in VSCode.
To prevent the user from seeing that, I changed the exports to:

export const Thing: new (owner: ThingOwner) => Thing = GenericThing;
export type Thing = GenericThing<ThingOwner>;

Then it's shown as (alias) new Thing(owner: ThingOwner): Thing

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1