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

Private properties required when assigning an instance of a different class with the same public interface #16556

Closed
Pmyl opened this issue Jun 15, 2017 · 5 comments
Labels
Duplicate An existing issue was already created

Comments

@Pmyl
Copy link

Pmyl commented Jun 15, 2017

TypeScript Version: 2.3.4

Code

class ComplexClass {
  public firstField: string;
  public secondField: string;

  private thirdField: string;
}

class NormalClass {
  public firstField: string;
  public secondField: string;
}

function complexFunction(param: ComplexClass) {
  console.log(param.firstField);
  console.log(param.secondField);
  // done, I can't use other properties of a ComplexClass object
}

complexFunction(new NormalClass());

Expected behavior:
Typescript compiles correctly.

Actual behavior:

Argument of type 'NormalClass' is not assignable to parameter of type 'ComplexClass'.
Property 'thirdField' is missing in type 'NormalClass'

Comments:
The public interface of ComplexClass is what the function complexFunction is expecting to use (properties firstField and secondField) but Typescript is expecting the developer to pass a parameter that mirrors also the private properties.
Is this done on purpose?

To make it work I need to create an unnecessary interface:

interface NormalInterface {
  public firstField: string;
  public secondField: string;
}

class ComplexClass implements NormalInterface {
  public firstField: string;
  public secondField: string;

  private thirdField: string;
}

class NormalClass implements NormalInterface {
  public firstField: string;
  public secondField: string;
}

function complexFunction(param: NormalInterface) {
  console.log(param.firstField);
  console.log(param.secondField);
}

complexFunction(new NormalClass());

Not always I have control on the type of the input parameter of the function so this solution is not applicable. Why the parameter of the function does not accept an object with the same public interface?

@jcalz
Copy link
Contributor

jcalz commented Jun 15, 2017

That's the intended design for protected/private fields of classes. If you want the public part of a class, you can make a new type alias:

type PublicPart<T> = {[K in keyof T]: T[K]}; //keyof only sees public properties

class ComplexClass {
  public firstField: string;
  public secondField: string;
  private thirdField: string;
}

class NormalClass {
  public firstField: string;
  public secondField: string;
}

function complexFunction(param: PublicPart<ComplexClass>) {
  console.log(param.firstField);
  console.log(param.secondField);
  // done, I can't use other properties of a ComplexClass object
}

complexFunction(new NormalClass()); // no error

But I think the "unnecessary" interface is actually what you want, since it specifies the shape of the data you care about. Even with PublicPart<> I think you'd want to say

class NormalClass implements PublicPart<ComplexClass>

or, forget about PublicPart<> and just do this:

class NormalClass {
  public firstField: string;
  public secondField: string;
}

class ComplexClass extends NormalClass {
  private thirdField: string;
}

function normalFunction(param: NormalClass) {
  console.log(param.firstField);
  console.log(param.secondField);
  // done
}

normalFunction(new NormalClass()); // no error
normalFunction(new ComplexClass()); // no error

@Pmyl
Copy link
Author

Pmyl commented Jun 15, 2017

I understand what your saying but not always I have access to the type of the parameter, if an external library instead of typing a parameter with an interface is using a class with private/protected fields (I know that it's wrong but it could happens) and for example I want to make a dummy/stub for it I need to either:
• extend the real class (not viable since you don't know what happens in the constructor)
• cast it (not ideal)

In a different language it would be different but this is typescript and in absence of private fields I can assign the parameter without any problems.

Another example:

// External library
class ComplexClass {
  public firstField: string;
  public secondField: string;
}

// External library
function complexFunction(param: ComplexClass) {
  console.log(param.firstField);
  console.log(param.secondField);
  // done, I can't use other properties of a ComplexClass object
}

// Personal Application
class NormalClass {
  public firstField: string;
  public secondField: string;
}

// Personal Application
complexFunction(new NormalClass()); // this works

This example works but it's odd that with a simple implementation change in the ComplexClass (adding the private thirdField) I need to change my NormalClass when in the eye of the complexFunction nothing changes, also the application would impossibly breaks unless there is a direct access to private fields inside that function so the forced change is useless.

I have read your link but I can find only how private fields work but not why it works like that, I really like to know the reason for that decision, I'm curious to see where I'm wrong at this point!

@jcalz
Copy link
Contributor

jcalz commented Jun 15, 2017

Read through #471 to see a discussion about this.

I think casting is the only solution if you don't have access to the class definition or parameter typings. You can do this sort of safely with something like:

function mock<T>(instance: PublicPart<T>): T {
    return <T>instance;
}

complexFunction(mock<ComplexClass>(new NormalClass()));

@DanielRosenwasser DanielRosenwasser added the Duplicate An existing issue was already created label Jun 16, 2017
@Pmyl
Copy link
Author

Pmyl commented Jun 16, 2017

I found an example in that discussion, I translate it with my example classes:

class ComplexClass {
  public firstField: string;
  public secondField: string;
  private thirdField: Function = () => {};

  public static doSomething(cc: ComplexClass): void {
    cc.thirdField();
  }
}

class NormalClass {
  public firstField: string;
  public secondField: string;
}

const normal: NormalClass = new NormalClass();
ComplexClass.doSomething(normal); // Fails

Now I understand why we can't accept an object based only on his public interface. Thanks for the answers!

@mhegazy
Copy link
Contributor

mhegazy commented Aug 17, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Aug 17, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants