Pass a field by reference in TypeScript

November 17th 2023 TypeScript

In TypeScript, there is no way to pass a variable to a method by reference so that the method could both read its value and write a new value to it. For class fields, such functionality can be achieved to some extent by passing the field name to the method while still preserving type safety.

Here is a slightly contrived example where such a functionality could be useful:

export interface ChangeNotification {
  field: string | number | symbol;
  value: any;
}

export class NotifyingClass {
  public notifier: (notification: ChangeNotification) => void | undefined;

  private numericField: number;

  public set numericProperty(value: number) {
    if (this.numericField === value) return;
    this.numericField = value;
    this.notifier?.({ field: "numericField", value });
  }

  public get numericProperty(): number {
    return this.numericField;
  }

  private stringField: string;

  public set stringProperty(value: string) {
    if (this.stringField === value) return;
    this.stringField = value;
    this.notifier?.({ field: "stringField", value });
  }

  public get stringProperty(): string {
    return this.stringField;
  }
}

This class will call the notifier function every time a value of a property value changes. Notice the almost identical code in both setters. It would have to be repeated with minor modifications in every other setter, which makes it very error-prone.

In many programming languages, you could move this code into a method and pass the underlying field by reference so that the code could both read from it and write to it. There might even be a way to get the field name in a type safe manner. Here is an example of a method doing this in C#:

private void SetValueWithNotification<T>(
  ref T field,
  T value,
  [CallerMemberName] string propertyName = "")
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return;
    field = value;
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Unfortunately, this exact approach can't be used in TypeScript, since there is no support for passing variables by reference. However, a class method can access a field by name.

Of course, adding a string parameter for this purpose would not be type safe. However, the TypeScript type system allows us to restrict the value to only valid class member names. And it also provides a way to get its type so that we can restrict the new value to a matching type. Using all that, we can implement the following method:

private setValueWithNotification<K extends keyof this>(
  field: K,
  value: this[K]
) {
  if (this[field] === value) return;
  this[field] = value;
  this.notifier?.({ field, value });
}

The generic argument K extends keyof this restricts the field parameter to valid member names, and the this[K] type requires the value parameter to have a matching type. With the help of this method, we can simplify our setters:

public set numericField(value: number) {
  this.setValueWithNotification("numericField", value);
}

public set stringField(value: string) {
  this.setValueWithNotification("stringField", value);
}

The only downside of this approach is that we have to make the fields public, otherwise the keyof type operator will not allow them.

public numericField: number;
public stringField: string;

The helper method (and the notifier field) can even be moved to a base class:

export class BaseClass {
  public notifier: (notification: ChangeNotification) => void | undefined;

  protected setValueWithNotification<K extends keyof this>(
    field: K,
    value: this[K]
  ) {
    if (this[field] === value) return;
    this[field] = value;
    this.notifier?.({ field, value });
  }
}

Multiple classes can then reuse the same logic by deriving from this base class:

export class NotifyingClass extends BaseClass {
  public numericField: number;

  public set numericProperty(value: number) {
    this.setValueWithNotification("numericField", value);
  }

  public get numericProperty(): number {
    return this.numericField;
  }

  public stringField: string;

  public set stringProperty(value: string) {
    this.setValueWithNotification("stringField", value);
  }

  public get stringProperty(): string {
    return this.stringField;
  }
}

Full code for the example in this post is available in my GitHub repository. Each step in the path to the final solution is available as a separate commit.

JavaScript is much different from the typical object-oriented languages. If you're coming from a strongly-typed object-oriented background, the concepts you are familiar with will have to be reimplemented in JavaScript differently. Fortunately, TypeScript does a great job at allowing the final code to still be type safe.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License