6

I need to use some subset of class properties names as values in a map to use inside of the class. In following example I've replaced map by array. The problem is that if property is marked private it's not listed in keyof list. How can I specify type of keys if I need to include private names?

var keys: Array<keyof A> = ["x", "y"]; // Error

class A {
  private x = 7;
  public y = 8;

  private keys: Array<keyof A> = ["x", "y"]; // Error
}

There is the same error both for variable outside of the class and for private property inside of it:

Type '"x"' is not assignable to type '"y"'.

Qwertiy
  • 19,681
  • 15
  • 61
  • 128
  • `private` and `protected` properties are hard to deal with programmatically since, as you noticed, they don't appear in `keyof`. You might consider not making `x` private at all, and instead export a version of `A` that omits `x`. But that depends on your use case, which I don't know. – jcalz Jul 17 '19 at 00:11
  • Compare [this](http://www.typescriptlang.org/play/#code/HYQwtgpgzgDiDGEAEAFATgSwG4gC7SQG8AoJJCADxgHs1cl4AbEKKJAQSNLKRkx3xIKALiRRcmYAHMkAXiQAiEAoDc3MjACuAI0YZ4SAJ6jgmsNoho5SAIxqevfnmQBrCIaij2aNCEMAeN0NqADMOAD5rAG0FCgUAGkVDBQBdFSQAegykAFEfWm4AX24cKyDPDh8-QPdQiOjYhKTU9Kzc-LRiYvhqYHFHbGdOeWAIAHdUJ3woADp2AAoASjU+Qfx2GcNW7NNzS2JVgQgNim3yDtEMXAByNkPnA6njmfKzyzRaS5u7p+JiIA)... – jcalz Jul 17 '19 at 00:19
  • ...to [this](http://www.typescriptlang.org/play/#code/HYQwtgpgzgDiDGEAEA5A9gFwKIA8ZoCcMIATJAbwCgkl4AbEKKJAfQEELqakcAuJKBgIBLYAHMkAXiQAiEDIDcXGgE9+wAK5gARhAJSkARiXckAawgqo-NgQIgVAHgsq0AM1ZsAfAYDaMnBkAGlkVGQBdBSQAemikNDMHLgBfLgg8QgwkUWICNwRkDnTiYBJmAHkwYQxHdhCAmSQAH1kXKBkfclSadPwiWjRgQSQ2dQgAdyQACgBKKR8OaXYTJAA3EH02mzsHZ0t3Tx9pf0D6sMiYuISk1Mp4QeHgTFw+4hJFpGAJ1GeMolIAHRsWZKJ7YP5vNgAlRRWKfLS6AiUMEvTKkKE4WFxPQEQj8GC4mB6DAqJAkNDQT6YJDpYSCZG-V7ogFtLE0ux4pAEtBEoik8mUsE0nB0jBAA). Does that meet your needs? If so I'll make an answer; otherwise the answer is probably just "no you can't do this, sorry" – jcalz Jul 17 '19 at 00:19
  • @jcalz, interesting idea. I think you can post it as an answer. – Qwertiy Jul 17 '19 at 00:21

1 Answers1

6

As you noticed, private and protected properties of a class C do not appear as part of keyof C. This is usually desirable behavior, since most attempts to index into a class with a private/protected property will cause a compile error. There is a suggestion at microsoft/TypeScript#22677 to allow mapping a type to a version where the private/protected properties are public, which would give you a way to do this... but this feature has not been implemented as of TypeScript 4.9.

So this doesn't work:

namespace Privates {
  export class A {
    private x: string = "a";
    public y: number = 1;
    private keys: Array<keyof A> = ["x", "y"]; // Error
  }
  var keys: Array<keyof A> = ["x", "y"]; // Error
}
const privateA = new Privates.A();
privateA.y; // number
privateA.x; // error: it's private
privateA.keys; // error: it's private

But maybe you don't actually need the properties to be private, so much as not visible to outside users of the class. You can use a module/namespace to export only the facets of your class that you want, like this:

namespace NotExported {
  class _A {
    x: string = "a";
    y: number = 1;
    keys: Array<keyof _A> = ["x", "y"]; // okay
  }
  export interface A extends Omit<_A, "x" | "keys"> {}
  export const A: new () => A = _A;
  var keys: Array<keyof _A> = ["x", "y"]; // okay
}

const notExportedA = new NotExported.A();
notExportedA.y; // number
notExportedA.x; // error: property does not exist
notExportedA.keys; // error: property does not exist

In NotExported, the class constructor _A and the corresponding type _A are not directly exported. Internally, keyof _A contains both the "x" and "y" keys. What we do export is a constructor A and a corresponding type A that omits the x property (and keys property) from _A. So you get the internal behavior you desire, while the external behavior of NotExported.A is similar to that of Privates.A. Instead of x and keys being inaccessible due to private violation, they are inaccessible because they are not part of the exported A type.

I actually prefer the latter method of not exporting implementation details rather than exposing the existence of private properties, since private properties actually have a lot of impact on how the corresponding classes can be used. That is, private is about access control, not about encapsulation.

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I want to create a Record inside a class to map the enum value to private methods to avoid long switch-case. What are the alternatives here? – vyenkv Oct 06 '22 at 08:02
  • 1
    If you have a separate/follow-up question, the comments section of a years-old answer isn't the best place to ask it. Assuming you can't find the answer anywhere else, you might want to post your own question, with a full [mre] of the situation you're talking about. That way you'll get more eyes on it than just mine, and you'll have the room to present your question in full. Good luck! – jcalz Oct 06 '22 at 16:16