First of all, we should be clear that private and protected are only Reserved words in javascript at this stage, rather than Keywords. So pure type declarations in TypeScript are erased when compiled.

class Person {
  public name: string;
  protected age: number; 
  private isMarried: boolean;
}
// Compile the result
class Person {}Copy the code

TypeScript is a structured type language. When comparing two different types, regardless of where they come from, the types themselves are said to be compatible if the types of all members are compatible.

interface Named {
  name: string;
}

class Bar {
  name: string;
}

class Foo {
  name: string;
}

// OK, because of structural typing
let a: Named = new Person(); / / ✔ ️
let b: Foo = new Bar(); / / ✔ ️
Copy the code

Because TypeScript property declarations are public by default, they are accessible as B. name, whereas Java is protected by default.

However, when comparing types with private or protected members, these types are treated differently. If one type has a private member, the other type must have a private member from the same declaration. The same applies to protected members.

class Bar {
  private name: string;
}

class Foo {
  private name: string;
}

let bar: Bar = new Foo(); / / ❌
//Type 'Foo' is not assignable to type 'Bar'.
  //Types have separate declarations of a private property 'name'.
Copy the code

The conceptual rules above come from the TypeScript Handbook and are just a brief introduction.

Why TypeScript handles private and protected rules differently than public when determining type compatibility, and what the potential benefits are.

Consider a scenario where electric vehicles are still in the early stages of development. Tesla and NiO have the same maximum mileage.

interface Car {
  maxMileage: number;
}

class Tesla implements Car {
   maxMileage: number = 500;
}

class Nio implements Car {
   maxMileage: number = 500;
}

function drive(car :Tesla) {
   console.log(car.maxMileage)
}

let tesla = new Tesla();
let nio = new Nio();
drive(tesla); / / ✔ ️
drive(nio); / / ✔ ️
Copy the code

Since TypeScript is a structural language, Tesla and Nio have a maxMileage field with the same name and type. This validates even if the Drive input is declared to be of type Tesla. For now, even if misused, drive will perform the same without a problem, but as technology evolves, the maxMileage of the two brands will differ and drive behavior will vary. This bug will remain dormant until it causes a serious failure.

On the basis of the above example, 1) and 2) are added, and the brand attribute of private(protected may also be declared) is added to solve the scenario that has the same structure but wants to distinguish types, so as to achieve the effect of similar declarative type system. This is where you take advantage of the fact that private and protected properties must be declared in the same place to determine type compatibility.

class Tesla implements Car {
   private brand: string = "Tesla"; / / 1)
   maxMileage: number = 500;
}

class Nio implements Car {
   private brand: string = "Tesla";  / / 2)
   maxMileage: number = 500;
}

function drive(car :Tesla) {
   console.log(car.maxMileage)
}
let tesla = new Tesla();
let nio = new Nio();
drive(tesla); / / ✔ ️
drive(nio); / / ❌
//Argument of type 'Nio' is not assignable to parameter of type 'Tesla'.
  //Types have separate declarations of a private property 'brand'.

/ / the compiled
class Tesla {
    constructor() {
        this.brand = "Tesla";
        this.maxMileage = 500; }}class Nio {
    constructor() {
        this.brand = "Tesla";
        this.maxMileage = 500; }}Copy the code

Although this is what we want, the class instance will have an extra brand attribute, which increases the runtime overhead. If this is not what you want, you can handle it as follows:

class Tesla implements Car {
  //@ts-ignore
   private brand: string;
   maxMileage: number = 500;
}

class Nio implements Car {
   //@ts-ignore
   private brand: string ;
   maxMileage: number = 500;
}

/ / the compiled
class Tesla {
    constructor() {
        this.maxMileage = 500; }}class Nio {
    constructor() {
        this.maxMileage = 500; }}Copy the code

You can see that the compiled code is clean. / / @ ts – ignore only in strictPropertyInitialization: true need, to avoid a compiler error because of uninitialized attribute.

Types have separate declarations of a private property error also occurs when the extends class inherits. Strange at first glance, different postures but similar error messages.

class ElectricVehicle {
   private charge() {};
}

//Type 'FF91' is not assignable to type 'ElectricVehicle'.
 // Types have separate declarations of a private property 'charge'
class FF91 extends ElectricVehicle {   / / ❌
    private charge() {};
}

Copy the code

This can be fixed by changing private to protected or public. Many articles will say that this is because private is semantically private and not visible to subclasses, so you can’t override, whereas protected and public are semantically visible to subclasses, and subclasses know that they’re overwriting, and that’s one thing.

We assume that TypeScript allows override of private methods and that the above class declaration compiles. But when we execute the following statement, the above error occurs again.

let parent = new ElectricVehicle();
let child = new FF91();
parent = child; / / ❌
//Type 'FF91' is not assignable to type 'ElectricVehicle'.
 // Types have separate declarations of a private property 'charge'
Copy the code

In the original example, Foo and Bar were just two classes with similar structures and no inheritance relationship, so it was understandable that the types were incompatible. There is no self-circle between parent and child classes if they are not type compatible. So the compiler reports errors ahead of class declaration, avoiding delays to use. This is why the FF91 class declared inheritance with the same error message as before.

Sample Playground