0

I'm new to TypeScript and tried to implement a auto-generated builder pattern so that I can write something along the lines of

class Foo {
    @buildable('inBar')
    public bar = 'Uninitialized';

    constructor() {}
}

const FooBuilder = getBuilder<Foo>(Foo);

const foo = new FooBuilder().inBar('My Bar').build();

Writing the code for this was more or less straightforward, considering that not only am I new to TS but also been away from JS for some time.

But of course now the transpiler complains about not knowing the inBar function. I could use the [] operator like const foo = new FooBuilder()['inBar']('My Bar').build(); but that kind of syntax somewhat defeats the purpose of having a builder pattern in the first place, which is supposed to improve readability.

Is there some way to dynamically add functions to a type? Maybe using the reflect-metadata API? I'm aware that decorators are not supposed to add methods to the decorated class but in this case I'm trying to dynamically create a new class in order to avoid having to write and maintain all the boilerplate code associated with the builder class. Being able to dynamically add a type declaration would be quite useful here.

Marcus Ilgner
  • 6,935
  • 2
  • 30
  • 44
  • I suppose, [this issue](https://github.com/Microsoft/TypeScript/issues/4881) is exactly what are you asking about. For this moment, you should directly extend type of your class with decorator fields. – Limbo May 05 '19 at 11:55
  • Possible duplicate of [Extending type when extending an ES6 class with a TypeScript decorator](https://stackoverflow.com/questions/54892401/extending-type-when-extending-an-es6-class-with-a-typescript-decorator) – Limbo May 05 '19 at 11:58
  • @LevitatorImbalance i don't see how this question relate to one you ref. You suggesting this is also impossible? – hackape May 05 '19 at 16:04
  • @hackape the meta of this questions are simillar. Decorators does not (yet, according to issue I have mentioned) exend types of entities on which they are being used, so, basically, this question is simillar to one I have mentioned, I suppose :) Of course, as I said in first comment, I think there is only one way to achieve this - extend types manually. I asked something simillar question about `Array` decorated with `@observable` from `mobx`. – Limbo May 05 '19 at 20:55

1 Answers1

2

JS is very dynamic, however TS is not. Regarding the typing system in TS, it's mostly functional, or pure. AFAIK the only side-effect-ish feature is declaration merging. Dynamically adding declaration depends on this feature.

The idea is simply: as you dynamically create builders on JS side, to reflect these on-going events accordingly, you should also "dynamically" expand the registry interface on TS side.

Here's the gist. Check this TS playground for full example.

// Empty interface, to be expanded later
interface BuilderRegistry {}
const builderRegistry: IBuilderRegistry = {} as any

class Foo {
    @buildable('inBar')
    public bar = 'Uninitialized';
    constructor() {}
}

interface IFooBuilder extends Builder<Foo> {
    inBar(val: string): IFooBuilder
}

// Expand IBuilderRegistry interface
interface IBuilderRegistry {
    'Foo': { new(): IFooBuilder }
}

const FooBuilder = getBuilder<'Foo'>(Foo);
const foo = new FooBuilder().inBar('My Bar').build(); // all good
hackape
  • 18,643
  • 2
  • 29
  • 57
  • I have another post about TS trick that leverage declaration merging, featuring a more apt example. If you're interested you can [read it here](https://stackoverflow.com/a/55694876/3617380). – hackape May 05 '19 at 17:18
  • And yes, that is exactly what I meant :D – Limbo May 05 '19 at 21:05