Skip to content

Instantly share code, notes, and snippets.

@bojidaryovchev
Last active August 31, 2022 15:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bojidaryovchev/e005fc81675a16e5bb8ca09c9efb1787 to your computer and use it in GitHub Desktop.
Save bojidaryovchev/e005fc81675a16e5bb8ca09c9efb1787 to your computer and use it in GitHub Desktop.
Coding Convention Proposal
// 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