Last active
August 31, 2022 15:22
-
-
Save bojidaryovchev/e005fc81675a16e5bb8ca09c9efb1787 to your computer and use it in GitHub Desktop.
Coding Convention Proposal
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Here is an idea of a coding convention inspired by pure functions: | |
// Let's look at the following example: | |
interface Stuff {} | |
interface OtherStuff {} | |
class StuffApi { | |
getStuff(): Promise<Stuff> { | |
// api call | |
} | |
getOtherStuff(stuff: Stuff): Promise<OtherStuff> { | |
// api call | |
} | |
} | |
const stuffApi = new StuffApi(); | |
class Something { | |
stuff?: Stuff; | |
otherStuff?: OtherStuff; | |
// we dont need anything in order to invoke stuffApi.getStuff | |
async initStuff(): Promise<void> { | |
this.stuff = await stuffApi.getStuff(); | |
} | |
// here on the other hand we need this.stuff to be initialized in order to invoke stuffApi.getOtherStuff(this.stuff) | |
// the reason writing our methods like this is not optimal is because when we look at the method signatures | |
// we are not able to directly see what do they depend on internally, and we can easily make the mistake of trying to invoke | |
// a method that depends on something else before that something has been initialized, resulting in an error; | |
// it is also harder to see the correct order of invocation as we would have to check all methods involved manually | |
async initOtherStuff(): Promise<void> { | |
this.otherStuff = await stuffApi.getOtherStuff(this.stuff); | |
} | |
// lets now consider passing the dependencies as parameters - by doing this we are directly able | |
// to see what a method depends on and also seamlessly know about the order of invocation | |
// so what should be passed as a parameter and what should be directly accessed from the this? | |
// there are 2 approaches we can follow and now we will look at both of them and their pros and cons | |
async initOtherStuff(stuff: Stuff): Promise<void> { | |
this.otherStuff = await stuffApi.getOtherStuff(stuff); | |
} | |
// lets talk about readonly properties for a second.. let's have some readonly property | |
readonly someReadonlyProperty: string = 'some readonly value'; | |
} | |
// readonly properties can only be assigned at the moment of definition or in the constructor, | |
// however typescript is flexible and allows us to do the following: | |
const something: Something = new Something(); | |
(something as { someReadonlyProperty: string }).someReadonlyProperty = | |
'some new value'; | |
// this approach is really simple, involves just a type cast, however by following it | |
// we are actually mutating a readonly value.. why would we keep the value as readonly in that case? | |
// well let's consider we have a consumer class called SomethingElse which should never be able to mutate the someReadonlyProperty.. | |
// by defining it as readonly in the Something class it would be visible as readonly to the outside world, however inside the Something class | |
// we could simply do that casting and change it.. this approach is quite simple, however it encourages improper usage, because | |
// there is nothing stopping us by doing that in consumer classes as well, therefore we can easily neglect the readonly-ness of the property | |
class SomethingElse { | |
something: Something = new Something(); | |
} | |
// lets now look at an alternative approach - typescript also allows us to define something to be readonly | |
// at the interface level, so we could simply do the following: | |
interface ISomething { | |
readonly someReadonlyProperty: string; | |
} | |
class Something implements ISomething { | |
// typescript allows us to define something to be readonly at the interface level, but keep it non-readonly at the class level | |
someReadonlyProperty: string; | |
} | |
class SomethingElse { | |
something: ISomething = new Something(); | |
} | |
// as we can see this approach involves creating a separate interface in which we expose our public api, | |
// implementing that interface in the Something class and then applying the dependency inversion principle in the | |
// SomethingElse class where we say something: ISomething - we achieve the same result, the someReadonlyProperty | |
// is treated as a non-readonly property in the Something class and as a readonly property in its consumer, the SomethingElse class | |
// however, the benefit of this approach is that at the class level we are directly able to know when something is going to be readonly | |
// and treat it as such, relying on the fact that it could only be assigned at the moment of definition or in the constructor | |
// so why are we talking about all this? well let's go back to our convention proposal, and more specifically to the question | |
// about what should be passed as a parameter and what should be directly accessed from the this | |
// if we follow the first approach with the casting, we cannot safely access readonly properties from the this | |
// as they may get changed at a certain point and therefore if we depend on them inside our methods we would have to | |
// invoke the methods anew, however we would not be able to directly see that | |
// on the other hand, if we follow the second approach with the readonly declaration at the interface level | |
// we are safe to directly access readonly properties from the this, because we would know that they will never change | |
// this way we can depend on them in our methods without having to worry about them changing | |
// no matter which approach we follow, we should pass things that may change over time as parameters to our methods | |
// we should only access the this in order to set certain properties, never get them | |
// if we follow the first approach we would also have to pass in readonly properties as we would not be 100% sure they would not change | |
// if we follow the second approach we can omit passing readonly properties as they will be surely treated like readonly | |
// and therefore only pass in whatever is going to change |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment