Dynamically initialize class properties using TypeScript decorators

Rares Matei
InstructorRares Matei
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 5 years ago

Decorators are a powerful feature of TypeScript that allow for efficient and readable abstractions when used correctly. In this lesson we will look at how we can use decorators to initialize properties of a class to promises that will make GET requests to certain URLs. We will also look at chaining multiple decorators to create powerful and versatile abstractions.

Instructor: [00:00] Here I have a REST API that returns an array of todos. I'm building this todo service and this specific property is supposed to return me those different todos from that API.

[00:12] I just need to initialize it. I could just write a fetch statement over here, but imagine our app has a lot more complexity around making API calls with secured authentication and caching, so we want to avoid doing all of that in this service.

[00:28] We ideally want some other middleware to take care of all of those details and it will just expose a simple function we can use to make GET requests like the ones for todos. Instead of a function, what I'll build is a get todos decorator that we can just attach to class properties wherever we want and it will be responsible for knowing how to make GET requests. Let's see how that's going to work.

[00:53] All a decorator is in Typescript is a function. In Typescript, decorator functions receive different arguments based on what they're decorating. In this case because it's a property decorator, it will be invoked with a target which will actually be the constructor of the class, the property we're decorating is on. In this case it will be the todo service constructor.

[01:15] This is really important. It's just a constructor function, not the actual instance. This function will be called when the whole file is loaded. That's very early when our program runs, while the actual instance might be created at a much later time.

[01:31] The second argument is the name of the property it's decorating. In my case name will be the todo string. I'll create an inner function here called init that will contain all the code necessary to make GET requests and instantiate our promise.

[01:50] I'll keep it simple in this case. It's just a fetch to the URL we looked at earlier, but you can encapsulate as much as you need to in here. Remember, this is supposed to be a reusable middleware. This is where you can place all of the complexity around authentication and caching.

[02:06] Given that this is just a constructor, how do I give the result of this function to instances of this? I'll use the object.define property syntax to define a getter for this property name on this class. Whenever somebody tries to get the property, I will return the result of this init function.

[02:29] Let's try it. I'll create a new instance of the class and try to access the todos property on it with a chained then statement because it's a promise. I'll open up my terminal and start up my file. Sure enough, if I scroll up through this list, I can see I get all of the todos from earlier.

[02:52] Now there's a problem. Every time I try to get this property, this getter will be called. I'm creating a new promise and a new network request for every get call. I should be able to just reuse the same promise once it's been created and fired up. I'll first need a place to store the promise once it's created.

[03:12] I'll create a property here called hidden instance key and I'll give it some obscure heart to replicate the name based on the name of the property I'm decorating. In my get function now the this context will represent my current instance.

[03:28] It's important to not use an arrow function here for the getter, because an arrow function will lock in your this context and will not let it be instance dependent. I'll first check if there's any existing value at this key in my current instance.

[03:45] If not, that means it's the first time somebody is trying to get this property, so I'll just initialize it, store it for future uses at the key I just created, and then return it to whoever was invoking the getter. If I have multiple get requests, they will all share the same underlying promise. Again, the this context that I'm using here in this case will refer to this specific instance.

[04:12] Finally, this is not that useful because it always makes requests to the same URL. Let's wrap this in a function and create a decorator factory which you can initialize with any URL. Now I'll go change it in my function as well.

[04:34] What we've ended up is a decorator that will initialize any property it's applied on with a promise that returns us the result of a get call to whatever URL was passed into here. Apart from looking a lot neater than a function call, property decorators have the advantage of being easily chainable.

[04:54] For example, I'll copy this and create a new decorator called first. This will get a reference to the previous getter initialized by the previous decorator in the chain, so when its init function is called it will first call the previous one and then attach another then operator at the end of it that queries the first element in the array.

[05:19] I'll also need to add configurable to both defined property calls so they can overwrite each other. Now I'll just go back to my class, pop this on top of the other decorator, open up my console, start up the file, and now we get three instances of the first todo because we are actually making the call three times here.

[05:42] You can actually create multiple decorators and stack them on top of each other and they will be called bottom to top.

Anup
Anup
~ 5 years ago

Inside @First decorator, prevInit() should be prevInit.call(target). Otherwise, value of "this" inside getter of @Get decorator will be undefined.

Tinkoff Dan
Tinkoff Dan
~ 5 years ago

Yes. There is truble with context into @First decorator. Could you please make changes to your lesson?

Rares Matei
Rares Mateiinstructor
~ 4 years ago

That's a very good point, Anup! Thank you for pointing that out!!

I updated the lesson code on GitHub to correct for this. It's also slightly different than your suggestion:

function First() {
  return function(target: any, name: string) {
    const hiddenInstanceKey = "_$$" + name + "$$_";
    const prevInit = Object.getOwnPropertyDescriptor(target, name).get;
    const init = (prevInit: () => any) => {
      return prevInit()
        .then(response => response[0]);
    };
    Object.defineProperty(target, name, {
      get: function() {
        return this[hiddenInstanceKey] || (this[hiddenInstanceKey] = init(prevInit.bind(this)));
      },
      configurable: true
    });
  }
}

Reason for this is that in your example, target will actually refer to the constructor of our service, and not the actual instance. And we need them to reffer to the same instance always inside our gets, so we can store our promise, and re-use it correctly.

Markdown supported.
Become a member to join the discussionEnroll Today