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.

4
  • I suppose, this issue is exactly what are you asking about. For this moment, you should directly extend type of your class with decorator fields. Commented May 5, 2019 at 11:55
  • Possible duplicate of Extending type when extending an ES6 class with a TypeScript decorator Commented May 5, 2019 at 11:58
  • @LevitatorImbalance i don't see how this question relate to one you ref. You suggesting this is also impossible? Commented May 5, 2019 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<SomeType> decorated with @observable from mobx. Commented May 5, 2019 at 20:55

1 Answer 1

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
Sign up to request clarification or add additional context in comments.

2 Comments

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.
And yes, that is exactly what I meant :D

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.