Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typescript version #25

Open
harrytang opened this issue Feb 8, 2020 · 11 comments
Open

typescript version #25

harrytang opened this issue Feb 8, 2020 · 11 comments
Labels
enhancement New feature or request wontfix This will not be worked on

Comments

@harrytang
Copy link

It would be nice if you can make typescript version of this, thank you!

@arcdev1
Copy link
Contributor

arcdev1 commented Feb 21, 2020

I don't have the time right now, but you're welcome to fork it and try and then I can add a link to your repo in my README.

@lffg
Copy link

lffg commented Mar 1, 2020

Hey @arcdev1, I'll try to make a TypeScript version of this project. I have just one question... Do you have any suggestion to where put the TS interfaces? Thanks! :)

@thierry-prost
Copy link

Would indeed be interesting to get a take on this.
I'm currently basing my archi on this and the interfaces are defined in the layer below their implementation. So for example data-access interfaces are defined in use-cases. Is there a better approach?

@arcdev1
Copy link
Contributor

arcdev1 commented Apr 11, 2020

According to Bob Martin, the Interface should be defined by its consumer. He goes on at length about this and many other wonderful little gems in this blog post.

So, yes, the use-case module should contain the data access Interface definition.

@arcdev1 arcdev1 added enhancement New feature or request wontfix This will not be worked on labels Dec 4, 2020
@sagiomc
Copy link

sagiomc commented Dec 19, 2020

Hello @arcdev1, first of all thank you for sharing this repo and your clear explanation in YouTube video 🙏 . All your advices had a huge impact in my organization to build better projects.

I've been working on making a Typescript version for 2 months now. I did an incomplete first version, but I had the code within my company's control version system, so I can't share that repo and I couldn't find the time to share this with the community.

Finally, I got the time and I would like to share my repo to contribute with the community and continue improve my skills with clean architectures using strongly typed languages by some feedback that you can share with me and others devs. 😄

Typescript Version

The tests are incomplete, I only develop the test for domain / use cases so I have to work on the rest.

@arcdev1
Copy link
Contributor

arcdev1 commented Dec 22, 2020

Hi @sagiomc ... I'm just seeing this now. I'm going to spend a bit of time with it in the coming days so I can give you thorough feedback, but I wanted to take a moment to thank you for doing this and to acknowledge all the hard work you put in. At first glance, there's lots of good stuff here. Thank you!

@arcdev1
Copy link
Contributor

arcdev1 commented Dec 30, 2020

Hi @sagiomc,

I've finally had some time to go over the Typescript codebase in more depth. I want to reiterate how much I appreciate the effort you put into this and how much good stuff is in there.

I think there are some small tweaks that would make it even better. But I realized, re-reading your original post, you did not specifically ask for feedback. So, before I start doling out advice, I thought I would ask; are you okay with me replying here with some suggestions to improve the code?

@sagiomc
Copy link

sagiomc commented Dec 30, 2020

Hi @arcdev1,

Thanks for taking the time and reviewing the codebase. I have no problem if you answer here, I'll appreciate all the advices to improve the code :)

@arcdev1
Copy link
Contributor

arcdev1 commented Dec 30, 2020

Thanks, @sagiomc. Here are my suggestions:

1. Don't allow Value Types to be instantiated with invalid data

Your code uses value types like UniqueEntityID which is great. But it does not prevent anyone from constructing those types using bad data. Instead, it asks consumers of the type to perform validation. This is dangerous and can lead to bugs and/or a lot of duplicate code.

Consider the code listing for UniqueEntityID:

export class UniqueEntityID extends Identifier<string | number> {
  public constructor(id?: string | number) {
    super(id ? id : uuidv4());
  }

  public isValidId(): boolean {
    return TextUtils.isValidUuid(this.toString());
  }
}

Nothing stops us from writing:

let id = new UniqueEntityID(" ");

This means everywhere we use an instance of UniqueEntityID we have to make sure we call isValidId() on it. That's a lot of duplication all over our codebase. If we happen to forget to call isValidId(), we may find ourselves with a bug where code is expecting an instance of UniqueEntityID to be a valid UniqueEntityID and it's not.

In general, a Type should serve as a contract that all instances conform to. So, I would rewrite the UniqueEntityID (and all other Value Types) this way:

export class UniqueEntityID extends Identifier<string | number> {

  public static of(id: string) {
    UniqueEntityID.validate(id)
    return new UniqueEntityID(id);
  }

  public static next() {
    return new UniqueEntityID(uuidv4());
  }

  protected constructor(id?: string | number) {
    super(id);
  }

  protected static validate(id: string) {
    if(!UniqueEntityID.isValid(id)) {
      throw new TypeError(`Invalid id.`);
    }
  }

  public static isValid(id: string): boolean {
    return TextUtils.isValidUuid(id);
  }
}

This way, it's impossible to create an invalid instance of UniqueEntityID and any code that receives a UniqueEntityID can trust that it's valid without having to validate it. Meanwhile, if someone wants to determine whether or not a string is a valid UniqueEntityID they can do that before creating an instance.

Author and Hash also seem like good candidates for value types since they have discrete rules. Overall, this is a very powerful approach and would lead to a cleaner Comment entity since most of the validation can occur before the constructor is even called.

As a side note, importing the uuid library here is violating the Dependency Rule from Clean Architecture. In a real-world JavaScript/TypeScript application, as long as this is the only place where that library gets imported, then it's probably an okay trade-off until UUID's make it into the Language itself. Bringing MD5 directly into the Comment entity on the other hand makes me a bit nervous, I think this points to a flaw in my own implementation as well. I don't like bringing services into domain objects. I think the hash function belongs in the Use Case where the MD5 library can be injected.

And finally, as a super picky, additional side note, "UniqueEntityID" is a very long name for something that's likely to get a lot of use. How about UniqueID or even just ID?

2. Omit the word "get" from readonly property names.

In general, it's good to have function names start with verbs, and property names start with nouns. But getters are a bit odd; they let us define a function that gets invoked through property access. So, for getters, I recommend using property naming conventions over function naming conventions. As a concrete example, in the Comment class, I would rename getAuthor to just author. So...

// current code
public get getAuthor(): string {
  return this.author;
}

// suggested code
public get author(): string {
  return this.author;
}

This allows consumers to write code like:

let comment = Comment.create(...);
let author = comment.author

This feels more natural to me. The reason I used getAuthor in my example is that I tend to avoid getters in plain JavaScript so getAuthor was a typical function and was called the typical way; getAuthor(). In Typescript with Classes, I like getters just fine because your code won't compile if you try to assign a value.

3. Take advantage of import type

If you use import type instead of import when importing a module that only contains type information, it guarantees that the import will be removed at compile time. This avoids some subtle bugs.

And more importantly, if you import a class and only end up using it's type information (as in AddCommentRepository.ts) you ensure no unnecessary code is pulled in and avoid potentially circular references.

Hope that's helpful.

Thanks again for all your work. The code is really coming along nicely.

Edit: typos.

@sagiomc
Copy link

sagiomc commented Dec 31, 2020

Thank you very much for your suggestions @arcdev1, I'll work on refactors following your advices in the coming weeks. Also, I have a couple of questions with value types and getters and setters:

1. Value Types

I wasn't aware of the potential bugs and duplicate code with the current implementation of the value types until your explanation, thanks for that, now it's very clear to me. Now, thinking about changing Author and Hasher in Value Types, I get the question if it is worth creating a new directory called value-types or value-objects inside src/domain/entities for Author case and inside src/shared/domain for the case Hasher and UniqueEntityID (I'm gonna change the name of this by ID). I ask this because when I started learning about clean architecture I had a hard time understanding the difference between entities and use cases and I don't know if it is worth being clear about the project structure also with the value types.

2. Getter's and Setter's

I'm completely agree with the naming convention here. The only reason I don't use get author like the suggested code was because in Typescript the getter's/setter's cannot have the name that matches the property name, I was getting the Error: TS2300: Duplicate identifier 'author'. The general pattern according with documentation is to name the internal properties just like a getter/setter only with the edition of an underscore:

//property
private _author: string;

public get author(): string {
  return this._author;
}

I think this is because JavaScript lacks runtime encapsulation, an underscore usually signifies internal/private/encapsulated/property/variable. In my experience dangling underscores may be the most polarizing in JavaScript, so I decided avoid use dangling underscore for that reason... the single underscore prefix has become popular as a way to indicate "private" members of objects so perhaps I should start use this pattern too 😅

@arcdev1
Copy link
Contributor

arcdev1 commented Jan 1, 2021

Hi @sagiomc, to answer your questions...

I wouldn't bother creating a new directory for value types. In DDD we have a distinction between entities, aggregates, and value types, but in Clean Architecture, they're all just entities. As for use cases, they are generally considered Application Services in a DDD context if that makes sense.

Regarding Getters and Setters, TypeScript now has support for the proposed ECMAScript private Class Fields. So you can actually achieve runtime encapsulation today. If you prefix a class field with a hash (instead of an underscore) it will be truly private. Some code to show you what I mean:

class Privacy {
    private _tsPrivate = "I'm only private in TypeScript at design time";
    #reallyPrivate = "I'm private in TypeScript AND in JavaScript at design time AND runtime";
}

let p = new Privacy()
p._tsPrivate // no errors at runtime
p.reallyPrivate // will throw an error at runtime

You can read more about JavaScript private Class Fields on the 2ality blog at https://2ality.com/2019/07/private-class-fields.html

Hope that's helpful. Let me know if you have any other questions and good luck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

5 participants