DEV Community

Kamil Trusiak
Kamil Trusiak

Posted on

How to get class fields of a given type?

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() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

Step 3. IsLoadingRecord

Take a look at the code below:

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

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() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Let me explain how it works.

ClassMethods<Omit<Type, 'isLoading'>>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Next, I use Record with booleans as values

Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>
Enter fullscreen mode Exit fullscreen mode

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

Partial<Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>>
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)