2
const log = {
  counter: {
    a: 1,
    b: 2,
    c: 3,
  },
  increment(entry: keyof typeof this.counter){
    this.counter[entry]++;
  }
};

function incrementLog(entry:keyof typeof log.counter){
    log.counter[entry]++;
}

incrementLog('a'); // ok
incrementLog('d'); // error, must be 'a' | 'b' | 'c'
log.increment('a'); // ok
log.increment('d'); // no error

Playground Link

I want to enforce the argument type of increment method to be keyof typeof log.counter, which is 'a' | 'b' | 'c'. I can achieve it in the standalone function, but it doesn't work in the increment method: 'this' is not defined.

I've also tried log.counter instead of this.counter on the method definition, but that creates a 'circular initializer' which also doesn't work as intended.

I hope not to manually type the log or manually type the counter, because when I make changes to the object, I hope to only make changes in one place.

Quuxuu
  • 549
  • 1
  • 4
  • 10

3 Answers3

2

When writing object-oriented code in TypeScript, it's a lot easier to use class syntax than it is to force things to work with plain object literals:

class Log {
  counter = {
    a: 1,
    b: 2,
    c: 3
  };

  increment(entry: keyof Log['counter']) {
    this.counter[entry]++;
  }
}

const log = new Log();

function incrementLog(entry:keyof Log['counter']) {
  log.counter[entry]++;
}

incrementLog('a'); // ok
incrementLog('d'); // error
log.increment('a'); // ok
log.increment('d'); // error
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Thanks very much! I accepted Nenad's answer because it's closest to the original question, but your answer is also great! – Quuxuu Apr 13 '20 at 20:08
  • @Quuxuu I agree, I hadn't thought of his approach when I posted this, and I like it too which is why I upvoted it. However, if you end up adding a lot of properties to `log`, this approach ends up being a little cleaner because it avoids cluttering the scope with a lot of temporary variables. – Patrick Roberts Apr 13 '20 at 20:11
  • Inspired by the accepted answer of [this question](https://stackoverflow.com/questions/1479319/simplest-cleanest-way-to-implement-singleton-in-javascript), I also found that if I remove counter completely from the object, and simply use the "temporary variable", it becomes "private states of a singleton" instead of "cluttering the scope". – Quuxuu Apr 14 '20 at 12:10
2

Define counter before log. You cannot reference type in the middle of expression which defines the type. You can easily avoid duplicating definition/initialization.

const counter = {
    a: 1,
    b: 2,
    c: 3,
};
const log = {
  counter,
  increment(entry: keyof typeof counter){
    this.counter[entry]++;
  }
};

function incrementLog(entry:keyof typeof log.counter){
    log.counter[entry]++;
}

incrementLog('a'); // ok
incrementLog('d'); // error, must be 'a' | 'b' | 'c'
log.increment('a'); // ok
log.increment('d'); // error, must be 'a' | 'b' | 'c'
Nenad
  • 24,809
  • 11
  • 75
  • 93
0

You should create a type or interface for log:

interface Log<T extends {[key: string]: number}> {
    counter: T;
    increment(element: keyof T): void;
}