'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 |
