Home > Enterprise >  How to use Typescript to type a class such that a function can only interact on the classes properti
How to use Typescript to type a class such that a function can only interact on the classes properti

Time:12-12

I'm struggling to type a class in a project I'm working on and looking for some expert help. While I do want to specifically know if it's possible to do what I'm asking (and how obviously), I am also open to advice on better ways to approach the problem.

My class has properties and a method to update these properties while also doing some side effects. It's due to the side effects I'm not just writing Foo.prop = "new val".

So, given a class:

class MyClass {
  prop1: string = "initial";
  prop2: boolean = false;

  constructor() {}

  update(key: ???, value: ???) {
    this[key] = value;
    this.doSomeSideEffects();
  }

  doSomeSideEffects() {
    ...
  }
}

Elsewhere in my code I'd like to call myClass.update('prop1', 'new string'); or something like that.

  1. How do I ensure that the key is a valid prop of the class and that the value is a valid type for the given prop?
  2. Is there a better way to do this that doesn't use magicky strings for the prop name/key?

So far I've come up with adding an interface and getting the keys off of that.

interface IMyClass {
  prop1: string;
  prop2: boolean;
}
type UpdateKeys = keyof IMyClass;

class MyClass implements IMyClass {
  ...
  update(key: UpdateKeys, value: typeof IMyClass[UpdateKeys]) {
    (this[key] as IMyClass[UpdateKeys]) = value;
    ...
  }
}

This allows intellisense to be more helpful when I call myClass.update() as it knows what keys are allowed, but the value type is string | boolean and it doesn't narrow that based on the prop passed. Plus, using as to typecast always feels like I could find a better way. (Though, as a bonus, using the interface helps me keep the class in sync slightly better?)

A last method I've thought of trying is to overload update() for each class property, but that is just going to get messy as more props are added (this class will hold configuration, so is going to grow over time).

class MyClass {
  ...
  update(key: "prop1", value: string);
  update(key: "prop2", value: boolean);
  update(key, value) { ...

Sorry this got so long! I wanted to make sure it was all well explained :) TIA for the advice and help!


*Edit: * The accepted answer also adds in a way to keep class methods from being allowed as the key for the update method, thus restricting it to only class properties. (This wasn't something I realised I wanted until seeing how the IDE handled the generic implementation)

CodePudding user response:

The approach I'd probably take here would be to make update() generic in the type K of key, which should be constrained to be known keys of the current object:

update<K extends keyof this>(key: K, value: this[K]) {
  this[key] = value;
  this.doSomeSideEffects();
}

Here's I'm using the polymorphic this type to represent the type of the current object (so it should continue to work for subclasses if they add properties), and saying that value should be of the indexed access type this[K], meaning the type of the property of this at key K.

Let's test it out:

const x = new MyClass();
x.update("prop1", "abc"); // okay
x.update("prop2", "abc"); // error
// -------------> ~~~~~
// Argument of type 'string' is not assignable to parameter of type 'boolean'
x.update("prop2", true); // okay

Looks good!


If you'd like to constrain K more so that nobody passes "update" or "doSomeSideEffects" in as key, you could use the Exclude<T, U> utility type to filter keyof this:

update<K extends Exclude<keyof this, "update" | "doSomeSideEffects">>(
  key: K, value: this[K]
) {

x.update("doSomeSideEffects", () => { }) // error!
// Argument of type '"doSomeSideEffects"' is
// not assignable to parameter of type '"prop1" | "prop2"'.

Playground link to code

  • Related