/kamil

How to get class fields of a given type?

2022-04-14
Originally published at dev.to

The Problem

I have a class with several methods that make requests to the API. I would like to be able to display a loading indicator for each of these calls independently. I don’t want to add new class field for each loading status indication property, and use one big object instead.

The question is: what type should I use for isLoading in my class definition?

export class LibraryStore {
  authors: Author[] = [];
  books: Book[] = [];

  isLoading: any /* ? */ = {};

  fetchAuthors() {
    this.isLoading.fetchAuthors = true;

    try {
      // ...
    } catch (e) {
      // ...
    } finally {
      this.isLoading.fetchAuthors = false;
    }
  }

  fetchBooks() {
    // ...
  }
}

The Solution

The easiest and most obvious way to make it work is to use Record<string, boolean>. But this type allows any string, not only methods’ names.

Let’s find out how to restrict this.

Step 1. KeyOfType

export type KeyOfType<Type, ValueType> = keyof {
  [Key in keyof Type as Type[Key] extends ValueType ? Key : never]: any;
};

The above code shows a generic type that extracts keys from Type, where Type[Key] is of type ValueType or a derivative thereof.

class SomeObject {
  a = 1;
  b = 'foo';
  c = 'bar';
  d = true;
}

const foobar: KeyOfType<SomeObject, string>; // 'b' | 'c'

And that’s it. KeyOfType is an answer to the question in title.

But let’s take it a little bit further.

Step 2. ClassMethods

This is a simple type, which extracts fields that are functions.

export type ClassMethods<T> = KeyOfType<T, Function>;

Step 3. IsLoadingRecord

Take a look at the code below:

export type IsLoadingRecord<Type> = Partial<Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>>;

This is final type I use for isLoading field.

export class LibraryStore {
  authors: Author[] = [];
  books: Book[] = [];

  isLoading: IsLoadingRecord<LibraryStore> = {};

  fetchAuthors() {
    this.isLoading.fetchAuthors = true;

    try {
      // ...
    } catch (e) {
      // ...
    } finally {
      this.isLoading.fetchAuthors = false;
    }
  }

  fetchBooks() {
    // ...
  }
}

Let me explain how it works.

ClassMethods<Omit<Type, 'isLoading'>>

ClassMethods type returns all methods from Type, except for isLoading. You may ask, why?

If isLoading is not omitted, using it inside Type (like in example above) would end with TypeScript error:

TS2502: 'isLoading' is referenced directly or indirectly in its own type annotation.

Next, I use Record with booleans as values

Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>

And finally, not all methods will use loading indicator, so I wrap it in Partial

Partial<Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>>

Now I can use properly typed isLoading field with autocomplete.

Autocomplete hints
Code editor shows proper autocomplete hints

The End

Hope you enjoyed this quick journey. If you have any problem I could help you with, please leave a comment.

See you next time!

Created with Elmstatic