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

Nominal type checking for private properties cannot be used in class libraries (TS2322) #28128

Closed
eladb opened this issue Oct 25, 2018 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@eladb
Copy link

eladb commented Oct 25, 2018

I'd like to add our two cents to the discussions in #8346 and #7755.

We understand the desire to ensure that code can trust that private properties have consistent behavior across versions, but practically this means that it won't be possible to use private members class libraries. This is because it is common that multiple versions of the same class library will exist in the same dependency closure. If a class has private members, than suddenly two versions of the same class are not interchangeable, even if they are fully compatible (private + public).

Specifically to our use case: we are building the AWS Cloud Development Kit. Almost all CDK classes inherit from a base class Construct which is defined in one of the core libraries. This base class contains private properties.

Constructs form a tree, which is structured by supplying the parent construct upon initialization:

Sketch of the base class:

class Construct {
  private _children: Construct[];
  constructor(parent: Construct);
}

Now, both users of the CDK and vendors of 3rd party CDK libraries create constructs by extending the Construct base class.

Here's a common use case:

import { ThirdPartyConstruct } from '3plibrary';

class MyApp extends Construct {
  constructor(parent: Construct) {
    super(parent);

    new ThirdPartyConstruct(this); // add this guy as a child

    // my logic
  }
}

Now, say this user depends on CDK 1.0.0 and 3plibrary depends on the CDK 1.0.1, the base classes are not compatible anymore and the user will get the infamous Type 'MyApp' is not assignable to type 'Construct'. Types have separate declarations of a private property '_children'..

This is a major limitation of course for such a use case. At the moment we are considering to basically convert all private properties to public methods with underscore prefixes, but naturally that's not what we prefer.

Related: aws/aws-cdk#979

@eladb eladb changed the title Nominal type checking for private properties cannot be used in base classes (TS2322) Nominal type checking for private properties cannot be used in class libraries (TS2322) Oct 25, 2018
@eladb
Copy link
Author

eladb commented Oct 25, 2018

Apologies for the accidental submission before finishing the issue 👎

@weswigham
Copy link
Member

The typical suggestion is to take an interface rather than the actual underlying class at your input positions; that way at your input positions where you've determined version mismatches shouldn't be an issue, you only need something conforming to the public API of your components, rather than a specific instance of them.

@weswigham weswigham added the Working as Intended The behavior described is the intended behavior; this is not a bug label Oct 25, 2018
@eladb
Copy link
Author

eladb commented Oct 25, 2018

So, effectively the parent would be something like IConstruct instead of Construct. Correct?

@weswigham
Copy link
Member

Yep:

interface IConstruct {
  // no members?
}
class Construct implements IConstruct {
  private _children: Construct[];
  constructor(parent: IConstruct);
}

class MyApp extends Construct {
  constructor(parent: IConstruct) {
    super(parent);

    new ThirdPartyConstruct(this); // add this guy as a child

    // my logic
  }
}

@eladb
Copy link
Author

eladb commented Oct 29, 2018

Is the proposal for all type references to only use interfaces? Sounds like a huge overhead for a large class library.

Here's another example:

  1. lib-foo exports a type Foo (with private properties). It is published in two versions lib-foo@1.0.0 and lib-foo@1.0.1 (say both are structurally compatible).
  2. lib-goo@1.0.0 exports a function giveMeFoo(foo: Foo). It depends on lib-foo@1.0.0

Now, my app takes a dependency on lib-goo@1.0.0 and lib-foo@1.0.1.

This code will fail:

import { Foo } from 'lib-foo';
import { giveMeFoo } from 'lib-goo';

giveMeFoo(new Foo()); // TS2322

This practically means that we lost npm's ability to have multiple versions of the same library co-exist in the same closure.

Do I understand this correctly, or am I missing something? Seems like the guidance would either be to declare an interface for each class or to not use private properties altogether.

@eladb
Copy link
Author

eladb commented Nov 7, 2018

For posterity: what we eventually decided to do is to declare any dependency that includes types that are part of a module’s public API also as peerDependencies.

We realized this is the right way to ensure that types are semantically compatible, which is superior to structurely-compatible.

@eladb eladb closed this as completed Nov 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants