1

I have a Typescript class with 4 different properties, like this:

class MyClass {
     private x: number;
     private y: number;
     private z: number;
     private w: number;
}

I want to create four functions that increment these properties:

   incrementX() { this.x++; }
   incrementY() { this.y++; )
   ...

However, I don't want the increment logic (++) to be duplicated, I want to place it in one function. If Typescript had ref parameters like C#, I would do something like:

   incrementX() { this.increment(ref this.x); }
   increment(p: ref number) { p++; }

Typescript does not support passing by reference. The non type-safe way of implementing this is:

   incrementX() { this.increment("x"); }
   increment(p: string) {
       const self = this as any;
       self[p]++;
   }

It's not type-safe. I can easily call increment('not-a-property') without getting an error from the compiler. I've added a runtime check to make sure self[p] is indeed a number, but I still want something the compiler can catch.

Is there a type-safe way of implementing this?

Note: obviously my actual code doesn't increment numbers, but rather does something quite elaborate - not on numbers but on another class type.

zmbq
  • 38,013
  • 14
  • 101
  • 171
  • 1
    You could create a union type, so you'd only be able to pass in `x` or `y` or `z` or `w`. However, it feels like there is a better way to go about this but there is not enough information about the actual problem. – VLAZ Oct 02 '19 at 20:57
  • A union type will improve the situation, you are correct. I will still need to define my properties twice - once in the class and another time in the union type, but it's better than what I have now. The actual problem isn't relevant here, I'm basically looking for a way to pass a property by reference. – zmbq Oct 02 '19 at 21:12

2 Answers2

3

You could use keyof and number extends maybe? To allow passing only keys of the class which are numbers.

Playground here

class MyClass {
  public a: number = 0;
  public b: number = 0;
  public c: string = "";

  public increment(
     key: {
      [K in keyof MyClass]-?: number extends MyClass[K] ? K : never
    }[keyof MyClass]
  ) {
    this[key]++;
  }
}

const test = new MyClass();

test.increment("a");
test.increment("b");
test.increment("c"); // fail
test.increment("d"); // fail
ed'
  • 1,815
  • 16
  • 30
  • Thanks, this works fine, if I make all my properties public. I don't want to make them public, so I'll resort to writing the union type manually – zmbq Oct 03 '19 at 07:46
  • 2
    You can always secure those properties after the fact: `export interface MySecureClass extends Omit {};` `export const MySecureClass: new () => MySecureClass = MyClass;` – bbrownd Oct 03 '19 at 08:17
  • I can't understand the syntax of type of `key` parameter. Could anyone please help? – Shahryar Saljoughi Aug 06 '21 at 14:24
3

A solution would be to type p with keyof MyClass.

     increment(p: keyof MyClass): void {
       this[p]++;
     }

But it wont work. Because your number fields are private, and because in keys of MyClass you have the function increment itself.

A solution would be to extract only fields that are numbers:

type OnlyNumberKeys<O> = {
  [K in keyof O]: O[K] extends number ? K : never;
}[keyof O];

Then use this type in increment function:

class MyClass {
     x: number;
     y: number;
     z: number;
     w: number;

     increment(p: OnlyNumberKeys<MyClass>): void {
       this[p]++;
     }
}

Now p accepts only 'x' | 'y' | 'z' | 'x'.

Note that I had to remove all private keywords. I don't know if there is a solution for that.

Richard Haddad
  • 904
  • 6
  • 13