How to get class fields of a given type?
Tags: typescript
Published: 2022-04-14 07:18:46

## 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?

```tsx
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`

```tsx
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.

```tsx
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.

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

### Step 3. `IsLoadingRecord`

Take a look at the code below:

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

This is final type I use for `isLoading` field.

```tsx
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.

```tsx
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 `boolean`s as values

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

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

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

Now I can use properly typed `isLoading` field with autocomplete.

| ![Autocomplete hints](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ili1lav7j175le2gsnr.png) |
| :--: |
| *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!