New Best Practice
Sign In

Protect Class Members and Expose Only the Public API

Wednesday, January 22, 2020

This best practice is at the core of object-oriented programming's (OOP) concept that "classes are open for extension but closed for modification". This best practice will focus on the TypeScript implementation but is applicable broadly to (almost) any programming language.

In TypeScript we can use the public, protected and private modifiers for class members (both properties and methods) to specify if the member is publicly available to consumers of the class, only available to extensions of the class, or even more strict, only available within the class.

Public by Default

The default access level of a class member in TypeScript is public. When we omit the modifier, then the class member is publicly available. And this might not be what our intention is. We do not want to expose the internal workings of our class, and we want to be able to modify the internal working as we see fit in the future without breaking changes to the consumers of our classes.

Private is Good

Privacy is a good thing, not only in terms of Internet privacy, but certainly in terms of the internal details and implementations of our classes. As such, it is recommended that all class members, both properties and methods, use the private access modifier unless the member must be exposed to the consumers of our class, and therefore making it part of the public interface of the class.

Protected for Extension

The protected access modifier is used when we want to expose a class member to extending (or inheriting) classes. Class inheritance is incredibly powerful and should be implemented when abstraction and implementation differ.

Code Examples

class UserProfile extends Component<AppProps, AppState> {
  
  constructor(props) {
    super(props);
  }

  save(user: Partial<User>): void {
    // code omitted for brevity
  }
  
  render() {
    return (
      <>
        <Avatar user={this.props.user} />
        <UserForm user={this.user} onSave={user => this.save(user)} />
      </>
    );
  }
}

avoid: exposing all class members publicly

class UserProfile extends Component<AppProps, AppState> {
  
  constructor(private readonly props) {
    super(props);
  }

  private save(user: Partial<User>): void {
    // code omitted for brevity
  }
  
  render() {
    return (
      <>
        <Avatar user={this.props.user} />
        <UserForm user={this.user} onSave={user => this.save(user)} />
      </>
    );
  }
}

do: specify access modifiers for class members

Instructions

  • Do:

    use the protected access modifier for class members that must be exposed to inheriting classes

  • Do:

    use the private access modifier for class members unless the member must be exposed to inheriting classes or to consumers of the class

  • Avoid:

    omitting the access modifier and exposing all class members publicly

  • Avoid:

    the protected access modifier until extension (inheritance) becomes necessary

  • Consider:

    using the private access modifier by default

  • Why:

    classes are open to extension but closed for modification

  • Why:

    public class members are vulnerable to breaking changes

  • Why:

    future modification to private class members avoids breaking changes to the public API

Brian Love

Brian is a software engineer and Google Developer Expert in Web Technologies and Angular with a passion for learning, writing, speaking, teaching and mentoring. He regularly speaks at conferences and meetups around the country, and co-authored "Why Angular for the Enterprise" for O'Reilly. When not coding, Brian enjoys skiing, hiking, and being in the outdoors. Brian recently launched lookout.dev where you can find best practices and expert advice on topics ranging from TypeScript, Angular, React, Node.js and more.

Google Developers Expert

Discussions are healthy ❤️

Brian Love ·

How does this work with the new TC39 proposal for private fields?